Skip to content

Instantly share code, notes, and snippets.

@dwolrdcojp
Created April 22, 2024 13:01
Show Gist options
  • Save dwolrdcojp/80373919354c2579987f1f395387381b to your computer and use it in GitHub Desktop.
Save dwolrdcojp/80373919354c2579987f1f395387381b to your computer and use it in GitHub Desktop.
Utilizing Zig Errors for sending unsuccessful response?
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