This commit is contained in:
Bob Farrell 2024-11-07 22:18:18 +00:00
parent cae8034b36
commit eac80226ee
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 tests = @import("commands/tests.zig");
pub const database = @import("commands/database.zig");
pub const auth = @import("commands/auth.zig");
pub const Environment = enum { development, testing, production };
@ -47,6 +48,7 @@ const Verb = union(enum) {
bundle: bundle.Options,
@"test": tests.Options,
database: database.Options,
auth: auth.Options,
g: generate.Options,
s: server.Options,
r: routes.Options,
@ -87,6 +89,7 @@ pub fn main() !void {
\\ routes List all routes in your app.
\\ bundle Create a deployment bundle.
\\ database Manage the application's database.
\\ auth Utilities for Jetzig authentication.
\\ test Run app tests.
\\
\\ 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,
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(
allocator: std.mem.Allocator,
options: Options,

View File

@ -78,25 +78,32 @@ const middleware_content =
\\ return middleware;
\\}
\\
\\/// Invoked immediately after the request head has been processed, before relevant view function
\\/// is processed. This gives you access to request headers but not the request body.
\\pub fn beforeRequest(self: *Self, request: *jetzig.http.Request) !void {
\\/// Invoked immediately after the request is received but before it has started processing.
\\/// Any calls to `request.render` or `request.redirect` will prevent further processing of the
\\/// 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});
\\ self.my_custom_value = @tagName(request.method);
\\}
\\
\\/// Invoked immediately after the request has finished responding. Provides full access to the
\\/// response as well as the request.
\\pub fn afterRequest(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
\\/// Invoked immediately before the response renders to the client.
\\/// The response can be modified here if needed.
\\pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
\\ request.server.logger.debug(
\\ "[middleware] my_custom_value: {s}, response status: {s}",
\\ .{ 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
\\/// done before the next request starts processing.
\\/// freed before the next request starts processing.
\\pub fn deinit(self: *Self, request: *jetzig.http.Request) void {
\\ 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;
_ = cwd;
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var secret: [128]u8 = undefined;
const len = 128;
var secret: [len]u8 = undefined;
for (0..128) |index| {
secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)];
for (0..len) |index| {
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 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 {

View File

@ -21,6 +21,7 @@ pub const kv = @import("jetzig/kv.zig");
pub const database = @import("jetzig/database.zig");
pub const testing = @import("jetzig/testing.zig");
pub const config = @import("jetzig/config.zig");
pub const auth = @import("jetzig/auth.zig");
pub const DateTime = jetcommon.types.DateTime;
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 Object = zmpl.Data.Object;
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,
rendered: bool = false,
redirected: bool = false,
failed: bool = false,
redirect_state: ?RedirectState = null,
middleware_rendered: ?struct { name: []const u8, action: []const u8 } = null,
middleware_rendered_during_response: bool = false,
middleware_data: jetzig.http.middleware.MiddlewareData = undefined,
rendered_multiple: bool = false,
rendered_view: ?jetzig.views.View = null,
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
/// trigger an error).
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;
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.?;
}
/// 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.
/// ```zig
/// return request.redirect("https://www.example.com/", .moved_permanently);
@ -194,7 +208,7 @@ pub fn redirect(
location: []const u8,
redirect_status: enum { moved_permanently, found },
) jetzig.views.View {
if (self.rendered) self.rendered_multiple = true;
if (self.rendered or self.failed) self.rendered_multiple = true;
self.rendered = true;
self.redirected = true;
@ -209,6 +223,19 @@ pub fn redirect(
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 };
pub fn renderRedirect(self: *Request, state: RedirectState) !void {

View File

@ -153,10 +153,10 @@ pub fn processNextRequest(
try self.renderResponse(&request);
try request.response.headers.append("Content-Type", response.content_type);
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 jetzig.http.middleware.afterResponse(&middleware_data, &request);
jetzig.http.middleware.deinit(&middleware_data, &request);
}
try self.logger.logRequest(&request);
@ -233,7 +233,7 @@ fn renderHTML(
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, .{})
else
request.setResponse(try self.renderNotFound(request), .{});
@ -302,6 +302,14 @@ fn renderView(
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|
zmpl.findPrefixed("views", request_template) orelse maybe_template
else

View File

@ -53,8 +53,8 @@ pub fn deinit(self: *Self) void {
}
/// Get a value from the session.
pub fn get(self: *Self, key: []const u8) !?*jetzig.data.Value {
if (self.state != .parsed) return error.UnparsedSessionCookie;
pub fn get(self: *Self, key: []const u8) ?*jetzig.data.Value {
std.debug.assert(self.state == .parsed);
return switch (self.data.value.?.*) {
.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.
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;
switch (self.data.value.?.*) {

View File

@ -1,9 +1,52 @@
const std = @import("std");
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 {
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;
}

View File

@ -12,6 +12,7 @@ stdout_colorized: bool,
stderr_colorized: bool,
level: LogLevel,
log_queue: *jetzig.loggers.LogQueue,
mutex: *std.Thread.Mutex,
/// Initialize a new Development Logger.
pub fn init(
@ -19,12 +20,14 @@ pub fn init(
level: LogLevel,
log_queue: *jetzig.loggers.LogQueue,
) DevelopmentLogger {
const mutex = allocator.create(std.Thread.Mutex) catch unreachable;
return .{
.allocator = allocator,
.level = level,
.log_queue = log_queue,
.stdout_colorized = log_queue.stdout_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 {
self.mutex.lock();
defer self.mutex.unlock();
// 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
// clobbering, but this is not the case here.