mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 05:56:07 +00:00
WIP
This commit is contained in:
parent
f3bcff6387
commit
6f8de03f07
10
cli/cli.zig
10
cli/cli.zig
@ -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
79
cli/commands/auth.zig
Normal 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,
|
||||
);
|
||||
}
|
@ -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,
|
||||
|
@ -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);
|
||||
\\}
|
||||
|
@ -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});
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
50
src/jetzig/auth.zig
Normal 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,
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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.?.*) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user