Created
April 22, 2024 13:01
-
-
Save dwolrdcojp/80373919354c2579987f1f395387381b to your computer and use it in GitHub Desktop.
Utilizing Zig Errors for sending unsuccessful response?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const std = @import("std"); | |
const time = std.time; | |
const zap = @import("../zap/zap.zig"); | |
const pg = @import("pg"); | |
const Conn = pg.Conn; | |
const StatusCode = zap.StatusCode; | |
const database = @import("../db/database.zig"); | |
const Result = database.Result; | |
const http_utils = @import("../utils/http_utils.zig"); | |
const HttpResponse = http_utils.HttpResponse; | |
const sendResponseBody = http_utils.sendResponseBody; | |
const auth = @import("../middleware/auth.zig"); | |
const parseJwt = auth.parseJWT; | |
const JwtOrResponse = auth.JwtOrResponse; | |
const cors = @import("../middleware/cors.zig").cors; | |
const password_utils = @import("../utils/password.zig"); | |
const hash = password_utils.hash; | |
const generateSalt = password_utils.generateSalt; | |
const ChangePasswordRequest = struct { | |
oldPassword: []u8, | |
newPassword: []u8, | |
}; | |
const ChangePasswordError = error{ | |
Unauthorized, | |
InvalidRequest, | |
DatabaseError, | |
PasswordMatch, | |
}; | |
pub const Self = @This(); | |
ep: zap.Endpoint = undefined, | |
arena: std.heap.ArenaAllocator = undefined, | |
pub fn init(allocator: std.mem.Allocator, path: []const u8) Self { | |
return .{ | |
.arena = std.heap.ArenaAllocator.init(allocator), | |
.ep = zap.Endpoint.init(.{ | |
.path = path, | |
.post = post, | |
.options = options, | |
}), | |
}; | |
} | |
pub fn deinit(self: *Self) void { | |
self.arena.deinit(); | |
} | |
pub fn endpoint(self: *Self) *zap.Endpoint { | |
return &self.ep; | |
} | |
fn createHttpResponse(message: []const u8, status: StatusCode) HttpResponse { | |
return HttpResponse{ | |
.message = message, | |
.status = status, | |
}; | |
} | |
fn post(e: *zap.Endpoint, r: zap.Request) void { | |
const self = @fieldParentPtr(Self, "ep", e); | |
defer _ = self.arena.reset(.retain_capacity); | |
var arenaAlloc = self.arena.allocator(); | |
var resp: HttpResponse = undefined; | |
if (changePassword(arenaAlloc, r)) |_| { | |
resp = createHttpResponse("Password changed.", StatusCode.ok); | |
} else |err| switch (err) { | |
error.DatabaseError => resp = createHttpResponse("Database error", StatusCode.internal_server_error), | |
error.Unauthorized => resp = createHttpResponse("Unauthorized token.", StatusCode.bad_request), | |
error.PasswordMatch => resp = createHttpResponse("Old password doesn't match.", StatusCode.bad_request), | |
else => resp = createHttpResponse("Internal server error", StatusCode.internal_server_error), | |
} | |
sendResponseBody(arenaAlloc, r, resp) catch |failed| { | |
std.log.err("Failed to send error to client: {}\n", .{failed}); | |
}; | |
} | |
fn isChangePasswordRequestValid(req: ChangePasswordRequest) bool { | |
return req.oldPassword.len > 0 and req.newPassword.len > 0; | |
} | |
fn changePassword(arenaAlloc: std.mem.Allocator, r: zap.Request) !bool { | |
var jwt: auth.Jwt = undefined; | |
if (parseJwt(arenaAlloc, r)) |jwtOrResp| { | |
if (jwtOrResp == JwtOrResponse.jwt) { | |
jwt = jwtOrResp.jwt; | |
} | |
} else |err| { | |
std.log.info("Invalid ChangePasswordRequest: {}\n", .{err}); | |
return ChangePasswordError.Unauthorized; | |
} | |
var request: ChangePasswordRequest = undefined; | |
if (r.body) |body| { | |
request = std.json.parseFromSliceLeaky(ChangePasswordRequest, arenaAlloc, body, .{}) catch |err| { | |
std.log.info("Invalid ChangePasswordRequest: {}\n", .{err}); | |
return ChangePasswordError.InvalidRequest; | |
}; | |
if (!isChangePasswordRequestValid(request)) { | |
return ChangePasswordError.InvalidRequest; | |
} | |
} | |
const conn = try database.aquireConnection(); | |
defer conn.release(); | |
const sql = | |
\\ SELECT | |
\\ u.password, | |
\\ u.salt | |
\\ FROM | |
\\ public."User" u | |
\\ WHERE | |
\\ u.username = $1 | |
\\ AND u.is_active = true | |
\\ LIMIT 1 | |
; | |
var result = conn.queryOpts(sql, .{jwt.payload.username}, .{ .allocator = arenaAlloc }) catch |err| { | |
if (err == error.PG) { | |
if (conn.err) |pge| { | |
std.log.err("PG {s}\n", .{pge.message}); | |
} | |
} | |
return ChangePasswordError.DatabaseError; | |
}; | |
defer result.deinit(); | |
if (try result.next()) |row| { | |
const data = .{ | |
.password = try arenaAlloc.dupe(u8, row.get([]const u8, 0)), | |
.salt = if (row.get(?[]const u8, 1)) |val| try arenaAlloc.dupe(u8, val) else null, | |
}; | |
try result.drain(); | |
// If salt exists validate the old password first | |
if (data.salt) |salt| { | |
const hashed = try hash(arenaAlloc, request.oldPassword, salt); | |
defer arenaAlloc.free(hashed); | |
// If password is a match then update password with new password | |
if (std.mem.eql(u8, hashed, data.password)) { | |
return try updatePassword(arenaAlloc, conn, jwt.payload.username, request.newPassword); | |
} else { | |
return ChangePasswordError.PasswordMatch; | |
} | |
} else { | |
// Already validated password without salt is a match on the client side | |
return try updatePassword(arenaAlloc, conn, jwt.payload.username, request.newPassword); | |
} | |
} | |
// Something went wrong | |
return ChangePasswordError.InvalidRequest; | |
} | |
fn updatePassword(alloc: std.mem.Allocator, conn: *Conn, username: []u8, password: []u8) !bool { | |
const salt = generateSalt(); | |
const newPassword = try hash(alloc, password, &salt); | |
defer alloc.free(newPassword); | |
const sql = | |
\\ UPDATE public."User" u | |
\\ SET password = $1, salt = $2 | |
\\ WHERE u.username = $3 | |
; | |
const rowsAffected = conn.execOpts( | |
sql, | |
.{ newPassword, salt, username }, | |
.{ .allocator = alloc }, | |
) catch |err| { | |
if (err == error.PG) { | |
if (conn.err) |pge| { | |
std.log.err("PG {s}\n", .{pge.message}); | |
} | |
} | |
return ChangePasswordError.DatabaseError; | |
}; | |
if (rowsAffected) |n| { | |
return n == 1; | |
} | |
return false; | |
} | |
pub fn options(e: *zap.Endpoint, r: zap.Request) void { | |
_ = e; | |
cors(r) catch return; | |
r.setStatus(zap.StatusCode.no_content); | |
r.markAsFinished(true); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment