This commit is contained in:
Bob Farrell 2024-11-07 22:18:18 +00:00
parent f3bcff6387
commit 6f8de03f07
14 changed files with 275 additions and 23 deletions

View File

@ -9,6 +9,7 @@ pub const routes = @import("commands/routes.zig");
pub const bundle = @import("commands/bundle.zig"); pub const bundle = @import("commands/bundle.zig");
pub const tests = @import("commands/tests.zig"); pub const tests = @import("commands/tests.zig");
pub const database = @import("commands/database.zig"); pub const database = @import("commands/database.zig");
pub const auth = @import("commands/auth.zig");
pub const Environment = enum { development, testing, production }; pub const Environment = enum { development, testing, production };
@ -47,6 +48,7 @@ const Verb = union(enum) {
bundle: bundle.Options, bundle: bundle.Options,
@"test": tests.Options, @"test": tests.Options,
database: database.Options, database: database.Options,
auth: auth.Options,
g: generate.Options, g: generate.Options,
s: server.Options, s: server.Options,
r: routes.Options, r: routes.Options,
@ -87,6 +89,7 @@ pub fn main() !void {
\\ routes List all routes in your app. \\ routes List all routes in your app.
\\ bundle Create a deployment bundle. \\ bundle Create a deployment bundle.
\\ database Manage the application's database. \\ database Manage the application's database.
\\ auth Utilities for Jetzig authentication.
\\ test Run app tests. \\ test Run app tests.
\\ \\
\\ Pass --help to any command for more information, e.g. `jetzig init --help` \\ Pass --help to any command for more information, e.g. `jetzig init --help`
@ -156,6 +159,13 @@ fn run(allocator: std.mem.Allocator, options: args.ParseArgsResult(Options, Verb
OptionsType, OptionsType,
options, options,
), ),
.auth => |opts| auth.run(
allocator,
opts,
writer,
OptionsType,
options,
),
}; };
} }
} }

79
cli/commands/auth.zig Normal file
View File

@ -0,0 +1,79 @@
const std = @import("std");
const args = @import("args");
/// Command line options for the `update` command.
pub const Options = struct {
pub const meta = .{
.usage_summary = "[password]",
.full_text =
\\Generates a password
\\Example:
\\
\\ jetzig update
\\ jetzig update web
,
.option_docs = .{
.path = "Set the output path relative to the current directory (default: current directory)",
},
};
};
/// Run the `jetzig database` command.
pub fn run(
allocator: std.mem.Allocator,
options: Options,
writer: anytype,
T: type,
main_options: T,
) !void {
_ = options;
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const alloc = arena.allocator();
const Action = enum { password };
const map = std.StaticStringMap(Action).initComptime(.{
.{ "password", .password },
});
const action = if (main_options.positionals.len > 0)
map.get(main_options.positionals[0])
else
null;
const sub_args: []const []const u8 = if (main_options.positionals.len > 1)
main_options.positionals[1..]
else
&.{};
return if (main_options.options.help and action == null) blk: {
try args.printHelp(Options, "jetzig database", writer);
break :blk {};
} else if (action == null) blk: {
const available_help = try std.mem.join(alloc, "|", map.keys());
std.debug.print("Missing sub-command. Expected: [{s}]\n", .{available_help});
break :blk error.JetzigCommandError;
} else if (action) |capture|
switch (capture) {
.password => blk: {
if (sub_args.len < 1) {
std.debug.print("Missing argument. Expected a password paramater.\n", .{});
break :blk error.JetzigCommandError;
} else {
const hash = try hashPassword(alloc, sub_args[0]);
try std.io.getStdOut().writer().print("Password hash: {s}\n", .{hash});
}
},
};
}
pub fn hashPassword(allocator: std.mem.Allocator, password: []const u8) ![]const u8 {
const buf = try allocator.alloc(u8, 128);
return try std.crypto.pwhash.argon2.strHash(
password,
.{
.allocator = allocator,
.params = .{ .t = 3, .m = 32, .p = 4 },
},
buf,
);
}

View File

@ -28,7 +28,7 @@ pub const Options = struct {
}; };
}; };
/// Run the `jetzig generate` command. /// Run the `jetzig database` command.
pub fn run( pub fn run(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
options: Options, options: Options,

View File

@ -78,25 +78,32 @@ const middleware_content =
\\ return middleware; \\ return middleware;
\\} \\}
\\ \\
\\/// Invoked immediately after the request head has been processed, before relevant view function \\/// Invoked immediately after the request is received but before it has started processing.
\\/// is processed. This gives you access to request headers but not the request body. \\/// Any calls to `request.render` or `request.redirect` will prevent further processing of the
\\pub fn beforeRequest(self: *Self, request: *jetzig.http.Request) !void { \\/// request, including any other middleware in the chain.
\\pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
\\ request.server.logger.debug("[middleware] my_custom_value: {s}", .{self.my_custom_value}); \\ request.server.logger.debug("[middleware] my_custom_value: {s}", .{self.my_custom_value});
\\ self.my_custom_value = @tagName(request.method); \\ self.my_custom_value = @tagName(request.method);
\\} \\}
\\ \\
\\/// Invoked immediately after the request has finished responding. Provides full access to the \\/// Invoked immediately before the response renders to the client.
\\/// response as well as the request. \\/// The response can be modified here if needed.
\\pub fn afterRequest(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { \\pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
\\ request.server.logger.debug( \\ request.server.logger.debug(
\\ "[middleware] my_custom_value: {s}, response status: {s}", \\ "[middleware] my_custom_value: {s}, response status: {s}",
\\ .{ self.my_custom_value, @tagName(response.status_code) }, \\ .{ self.my_custom_value, @tagName(response.status_code) },
\\ ); \\ );
\\} \\}
\\ \\
\\/// Invoked after `afterRequest` is called, use this function to do any clean-up. \\/// Invoked immediately after the response has been finalized and sent to the client.
\\/// Response data can be accessed for logging, but any modifications will have no impact.
\\pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) void {
\\ request.allocator.destroy(self);
\\}
\\
\\/// Invoked after `afterResponse` is called. Use this function to do any clean-up.
\\/// Note that `request.allocator` is an arena allocator, so any allocations are automatically \\/// Note that `request.allocator` is an arena allocator, so any allocations are automatically
\\/// done before the next request starts processing. \\/// freed before the next request starts processing.
\\pub fn deinit(self: *Self, request: *jetzig.http.Request) void { \\pub fn deinit(self: *Self, request: *jetzig.http.Request) void {
\\ request.allocator.destroy(self); \\ request.allocator.destroy(self);
\\} \\}

View File

@ -14,11 +14,12 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
_ = args; _ = args;
_ = cwd; _ = cwd;
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var secret: [128]u8 = undefined; const len = 128;
var secret: [len]u8 = undefined;
for (0..128) |index| { for (0..len) |index| {
secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)]; secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len - 1)];
} }
std.debug.print("{s}\n", .{secret}); try std.io.getStdOut().writer().print("{s}\n", .{secret});
} }

View File

@ -12,6 +12,10 @@ pub const database_lazy_connect = true;
pub const jetzig_options = struct { pub const jetzig_options = struct {
pub const Schema = @import("Schema"); pub const Schema = @import("Schema");
pub const middleware: []const type = if (@hasDecl(@import("main").jetzig_options, "middleware"))
@import("main").jetzig_options.middleware
else
&.{};
}; };
pub fn main() !void { pub fn main() !void {

View File

@ -21,6 +21,7 @@ pub const kv = @import("jetzig/kv.zig");
pub const database = @import("jetzig/database.zig"); pub const database = @import("jetzig/database.zig");
pub const testing = @import("jetzig/testing.zig"); pub const testing = @import("jetzig/testing.zig");
pub const config = @import("jetzig/config.zig"); pub const config = @import("jetzig/config.zig");
pub const auth = @import("jetzig/auth.zig");
pub const DateTime = jetcommon.types.DateTime; pub const DateTime = jetcommon.types.DateTime;
pub const Time = jetcommon.types.Time; pub const Time = jetcommon.types.Time;

50
src/jetzig/auth.zig Normal file
View File

@ -0,0 +1,50 @@
const std = @import("std");
const jetzig = @import("../jetzig.zig");
pub const IdType = enum { string, integer };
pub fn getUserId(comptime id_type: IdType, request: *jetzig.Request) !?switch (id_type) {
.integer => i128,
.string => []const u8,
} {
const session = try request.session();
return session.getT(std.enums.nameCast(jetzig.data.ValueType, id_type), "_jetzig_user_id");
}
pub fn signIn(request: *jetzig.Request, user_id: anytype) !void {
var session = try request.session();
try session.put("_jetzig_user_id", user_id);
}
pub fn verifyPassword(
allocator: std.mem.Allocator,
hash: []const u8,
password: []const u8,
) !bool {
const verify_error = std.crypto.pwhash.argon2.strVerify(
hash,
password,
.{ .allocator = allocator },
);
return if (verify_error)
true
else |err| switch (err) {
error.AuthenticationFailed, error.PasswordVerificationFailed => false,
else => err,
};
}
pub fn hashPassword(allocator: std.mem.Allocator, password: []const u8) ![]const u8 {
const buf = try allocator.alloc(u8, 128);
return try std.crypto.pwhash.argon2.strHash(
password,
.{
.allocator = allocator,
.params = .{ .t = 3, .m = 32, .p = 4 },
},
buf,
);
}

View File

@ -12,3 +12,4 @@ pub const Boolean = zmpl.Data.Boolean;
pub const String = zmpl.Data.String; pub const String = zmpl.Data.String;
pub const Object = zmpl.Data.Object; pub const Object = zmpl.Data.Object;
pub const Array = zmpl.Data.Array; pub const Array = zmpl.Data.Array;
pub const ValueType = zmpl.Data.ValueType;

View File

@ -35,9 +35,11 @@ layout: ?[]const u8 = null,
layout_disabled: bool = false, layout_disabled: bool = false,
rendered: bool = false, rendered: bool = false,
redirected: bool = false, redirected: bool = false,
failed: bool = false,
redirect_state: ?RedirectState = null, redirect_state: ?RedirectState = null,
middleware_rendered: ?struct { name: []const u8, action: []const u8 } = null, middleware_rendered: ?struct { name: []const u8, action: []const u8 } = null,
middleware_rendered_during_response: bool = false, middleware_rendered_during_response: bool = false,
middleware_data: jetzig.http.middleware.MiddlewareData = undefined,
rendered_multiple: bool = false, rendered_multiple: bool = false,
rendered_view: ?jetzig.views.View = null, rendered_view: ?jetzig.views.View = null,
start_time: i128, start_time: i128,
@ -173,7 +175,7 @@ pub fn respond(self: *Request) !void {
/// Render a response. This function can only be called once per request (repeat calls will /// Render a response. This function can only be called once per request (repeat calls will
/// trigger an error). /// trigger an error).
pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View { pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
if (self.rendered) self.rendered_multiple = true; if (self.rendered or self.failed) self.rendered_multiple = true;
self.rendered = true; self.rendered = true;
if (self.response_started) self.middleware_rendered_during_response = true; if (self.response_started) self.middleware_rendered_during_response = true;
@ -181,6 +183,18 @@ pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode)
return self.rendered_view.?; return self.rendered_view.?;
} }
/// Render an error. This function can only be called once per request (repeat calls will
/// trigger an error).
pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
if (self.rendered or self.redirected) self.rendered_multiple = true;
self.rendered = true;
self.failed = true;
if (self.response_started) self.middleware_rendered_during_response = true;
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
return self.rendered_view.?;
}
/// Issue a redirect to a new location. /// Issue a redirect to a new location.
/// ```zig /// ```zig
/// return request.redirect("https://www.example.com/", .moved_permanently); /// return request.redirect("https://www.example.com/", .moved_permanently);
@ -194,7 +208,7 @@ pub fn redirect(
location: []const u8, location: []const u8,
redirect_status: enum { moved_permanently, found }, redirect_status: enum { moved_permanently, found },
) jetzig.views.View { ) jetzig.views.View {
if (self.rendered) self.rendered_multiple = true; if (self.rendered or self.failed) self.rendered_multiple = true;
self.rendered = true; self.rendered = true;
self.redirected = true; self.redirected = true;
@ -209,6 +223,19 @@ pub fn redirect(
return .{ .data = self.response_data, .status_code = status_code }; return .{ .data = self.response_data, .status_code = status_code };
} }
pub fn middleware(
self: *const Request,
comptime name: jetzig.http.middleware.Enum,
) jetzig.http.middleware.Type(name) {
inline for (jetzig.http.middleware.middlewares, 0..) |T, index| {
if (@hasDecl(T, "middleware_name") and std.mem.eql(u8, @tagName(name), T.middleware_name)) {
const middleware_data = self.middleware_data.get(index);
return @as(*jetzig.http.middleware.Type(name), @ptrCast(@alignCast(middleware_data))).*;
}
}
unreachable;
}
const RedirectState = struct { location: []const u8, status_code: jetzig.http.status_codes.StatusCode }; const RedirectState = struct { location: []const u8, status_code: jetzig.http.status_codes.StatusCode };
pub fn renderRedirect(self: *Request, state: RedirectState) !void { pub fn renderRedirect(self: *Request, state: RedirectState) !void {

View File

@ -153,10 +153,10 @@ pub fn processNextRequest(
try self.renderResponse(&request); try self.renderResponse(&request);
try request.response.headers.append("Content-Type", response.content_type); try request.response.headers.append("Content-Type", response.content_type);
try jetzig.http.middleware.beforeResponse(&middleware_data, &request); try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
jetzig.http.middleware.deinit(&middleware_data, &request);
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
try request.respond(); try request.respond();
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
jetzig.http.middleware.deinit(&middleware_data, &request);
} }
try self.logger.logRequest(&request); try self.logger.logRequest(&request);
@ -233,7 +233,7 @@ fn renderHTML(
return request.setResponse(rendered_error, .{}); return request.setResponse(rendered_error, .{});
}; };
return if (request.redirected or request.dynamic_assigned_template != null) return if (request.redirected or request.failed or request.dynamic_assigned_template != null)
request.setResponse(rendered, .{}) request.setResponse(rendered, .{})
else else
request.setResponse(try self.renderNotFound(request), .{}); request.setResponse(try self.renderNotFound(request), .{});
@ -302,6 +302,14 @@ fn renderView(
return try self.renderInternalServerError(request, err); return try self.renderInternalServerError(request, err);
}; };
if (request.failed) {
const view: jetzig.views.View = request.rendered_view orelse .{
.data = request.response_data,
.status_code = .internal_server_error,
};
return try self.renderError(request, view.status_code);
}
const template: ?zmpl.Template = if (request.dynamic_assigned_template) |request_template| const template: ?zmpl.Template = if (request.dynamic_assigned_template) |request_template|
zmpl.findPrefixed("views", request_template) orelse maybe_template zmpl.findPrefixed("views", request_template) orelse maybe_template
else else

View File

@ -53,8 +53,8 @@ pub fn deinit(self: *Self) void {
} }
/// Get a value from the session. /// Get a value from the session.
pub fn get(self: *Self, key: []const u8) !?*jetzig.data.Value { pub fn get(self: *Self, key: []const u8) ?*jetzig.data.Value {
if (self.state != .parsed) return error.UnparsedSessionCookie; std.debug.assert(self.state == .parsed);
return switch (self.data.value.?.*) { return switch (self.data.value.?.*) {
.object => self.data.value.?.object.get(key), .object => self.data.value.?.object.get(key),
@ -62,8 +62,22 @@ pub fn get(self: *Self, key: []const u8) !?*jetzig.data.Value {
}; };
} }
/// Get a typed value from the session.
pub fn getT(
self: *Self,
comptime T: jetzig.data.ValueType,
key: []const u8,
) @TypeOf(self.data.value.?.object.getT(T, key)) {
std.debug.assert(self.state == .parsed);
return switch (self.data.value.?.*) {
.object => self.data.value.?.object.getT(T, key),
else => unreachable,
};
}
/// Put a value into the session. /// Put a value into the session.
pub fn put(self: *Self, key: []const u8, value: *jetzig.data.Value) !void { pub fn put(self: *Self, key: []const u8, value: anytype) !void {
if (self.state != .parsed) return error.UnparsedSessionCookie; if (self.state != .parsed) return error.UnparsedSessionCookie;
switch (self.data.value.?.*) { switch (self.data.value.?.*) {

View File

@ -1,9 +1,52 @@
const std = @import("std"); const std = @import("std");
const jetzig = @import("../../jetzig.zig"); const jetzig = @import("../../jetzig.zig");
const middlewares: []const type = jetzig.config.get([]const type, "middleware"); pub const middlewares: []const type = jetzig.config.get([]const type, "middleware");
pub const MiddlewareData = std.BoundedArray(?*anyopaque, middlewares.len);
pub const Enum = MiddlewareEnum();
const MiddlewareData = std.BoundedArray(?*anyopaque, middlewares.len); fn MiddlewareEnum() type {
comptime {
var size: usize = 0;
for (middlewares) |middleware_type| {
if (@hasDecl(middleware_type, "middleware_name")) size += 1;
}
var fields: [size]std.builtin.Type.EnumField = undefined;
var index: usize = 0;
for (middlewares) |middleware_type| {
if (@hasDecl(middleware_type, "middleware_name")) {
fields[index] = .{ .name = middleware_type.middleware_name, .value = index };
index += 1;
}
}
return @Type(.{
.@"enum" = .{
.tag_type = std.math.IntFittingRange(0, if (size == 0) 0 else size - 1),
.fields = &fields,
.decls = &.{},
.is_exhaustive = true,
},
});
}
}
pub fn Type(comptime name: MiddlewareEnum()) type {
comptime {
for (middlewares) |middleware_type| {
if (@hasDecl(
middleware_type,
"middleware_name",
) and std.mem.eql(
u8,
middleware_type.middleware_name,
@tagName(name),
)) {
return middleware_type;
}
}
unreachable;
}
}
pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData { pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
var middleware_data = MiddlewareData.init(0) catch unreachable; var middleware_data = MiddlewareData.init(0) catch unreachable;
@ -35,6 +78,7 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
} }
} }
request.middleware_data = middleware_data;
return middleware_data; return middleware_data;
} }

View File

@ -12,6 +12,7 @@ stdout_colorized: bool,
stderr_colorized: bool, stderr_colorized: bool,
level: LogLevel, level: LogLevel,
log_queue: *jetzig.loggers.LogQueue, log_queue: *jetzig.loggers.LogQueue,
mutex: *std.Thread.Mutex,
/// Initialize a new Development Logger. /// Initialize a new Development Logger.
pub fn init( pub fn init(
@ -19,12 +20,14 @@ pub fn init(
level: LogLevel, level: LogLevel,
log_queue: *jetzig.loggers.LogQueue, log_queue: *jetzig.loggers.LogQueue,
) DevelopmentLogger { ) DevelopmentLogger {
const mutex = allocator.create(std.Thread.Mutex) catch unreachable;
return .{ return .{
.allocator = allocator, .allocator = allocator,
.level = level, .level = level,
.log_queue = log_queue, .log_queue = log_queue,
.stdout_colorized = log_queue.stdout_is_tty, .stdout_colorized = log_queue.stdout_is_tty,
.stderr_colorized = log_queue.stderr_is_tty, .stderr_colorized = log_queue.stderr_is_tty,
.mutex = mutex,
}; };
} }
@ -105,6 +108,9 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request)
} }
pub fn logSql(self: *const DevelopmentLogger, event: jetzig.jetquery.events.Event) !void { pub fn logSql(self: *const DevelopmentLogger, event: jetzig.jetquery.events.Event) !void {
self.mutex.lock();
defer self.mutex.unlock();
// XXX: This function does not make any effort to prevent log messages clobbering each other // XXX: This function does not make any effort to prevent log messages clobbering each other
// from multiple threads. JSON logger etc. write in one call and the logger's mutex prevents // from multiple threads. JSON logger etc. write in one call and the logger's mutex prevents
// clobbering, but this is not the case here. // clobbering, but this is not the case here.