diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f24529b..538b79e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -18,6 +18,10 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: + # Create postgres server + # https://github.com/marketplace/actions/setup-postgresql-for-linux-macos-windows + - uses: ikalnytskyi/action-setup-postgres@v7 + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 with: @@ -43,7 +47,17 @@ jobs: - name: Run App Tests run: | cd demo + zig build -Denvironment=testing jetzig:database:create + zig build -Denvironment=testing jetzig:database:migrate zig build -Denvironment=testing jetzig:test + env: + JETQUERY_HOSTNAME: 'localhost' + JETQUERY_USERNAME: 'postgres' + JETQUERY_PASSWORD: 'postgres' + JETQUERY_DATABASE: 'test' + # Assume a small amount of connections are allowed + # into postgres + JETQUERY_POOL_SIZE: 2 - name: Build artifacts if: ${{ matrix.os == 'ubuntu-latest' }} diff --git a/cli/commands/init.zig b/cli/commands/init.zig index 42ae81e..934a3fe 100644 --- a/cli/commands/init.zig +++ b/cli/commands/init.zig @@ -106,7 +106,7 @@ pub fn run( try copySourceFile( allocator, install_dir, - "demo/config/database.zig", + "demo/config/database_template.zig", "config/database.zig", null, ); diff --git a/cli/compile.zig b/cli/compile.zig index 1067821..c89efb8 100644 --- a/cli/compile.zig +++ b/cli/compile.zig @@ -30,7 +30,7 @@ pub fn initDataModule(build: *std.Build) !*std.Build.Module { "demo/public/zmpl.png", "demo/public/favicon.ico", "demo/public/styles.css", - "demo/config/database.zig", + "demo/config/database_template.zig", ".gitignore", }; diff --git a/demo/config/database.zig b/demo/config/database.zig index 676b606..2c1e6d3 100644 --- a/demo/config/database.zig +++ b/demo/config/database.zig @@ -1,48 +1,7 @@ pub const database = .{ - // Null adapter fails when a database call is invoked. - .development = .{ - .adapter = .null, - }, + // This configuration is used for CI + // in GitHub .testing = .{ - .adapter = .null, - }, - .production = .{ - .adapter = .null, + .adapter = .postgresql, }, - // PostgreSQL adapter configuration. - // - // All options except `adapter` can be configured using environment variables: - // - // * JETQUERY_HOSTNAME - // * JETQUERY_PORT - // * JETQUERY_USERNAME - // * JETQUERY_PASSWORD - // * JETQUERY_DATABASE - // - // .testing = .{ - // .adapter = .postgresql, - // .hostname = "localhost", - // .port = 5432, - // .username = "postgres", - // .password = "password", - // .database = "myapp_testing", - // }, - // - // .development = .{ - // .adapter = .postgresql, - // .hostname = "localhost", - // .port = 5432, - // .username = "postgres", - // .password = "password", - // .database = "myapp_development", - // }, - // - // .production = .{ - // .adapter = .postgresql, - // .hostname = "localhost", - // .port = 5432, - // .username = "postgres", - // .password = "password", - // .database = "myapp_production", - // }, }; diff --git a/demo/config/database_template.zig b/demo/config/database_template.zig new file mode 100644 index 0000000..676b606 --- /dev/null +++ b/demo/config/database_template.zig @@ -0,0 +1,48 @@ +pub const database = .{ + // Null adapter fails when a database call is invoked. + .development = .{ + .adapter = .null, + }, + .testing = .{ + .adapter = .null, + }, + .production = .{ + .adapter = .null, + }, + // PostgreSQL adapter configuration. + // + // All options except `adapter` can be configured using environment variables: + // + // * JETQUERY_HOSTNAME + // * JETQUERY_PORT + // * JETQUERY_USERNAME + // * JETQUERY_PASSWORD + // * JETQUERY_DATABASE + // + // .testing = .{ + // .adapter = .postgresql, + // .hostname = "localhost", + // .port = 5432, + // .username = "postgres", + // .password = "password", + // .database = "myapp_testing", + // }, + // + // .development = .{ + // .adapter = .postgresql, + // .hostname = "localhost", + // .port = 5432, + // .username = "postgres", + // .password = "password", + // .database = "myapp_development", + // }, + // + // .production = .{ + // .adapter = .postgresql, + // .hostname = "localhost", + // .port = 5432, + // .username = "postgres", + // .password = "password", + // .database = "myapp_production", + // }, +}; diff --git a/demo/src/app/database/Schema.zig b/demo/src/app/database/Schema.zig new file mode 100644 index 0000000..436770d --- /dev/null +++ b/demo/src/app/database/Schema.zig @@ -0,0 +1,9 @@ +const jetquery = @import("jetzig").jetquery; + +pub const User = jetquery.Model(@This(), "users", struct { + id: i32, + email: []const u8, + password_hash: []const u8, + created_at: jetquery.DateTime, + updated_at: jetquery.DateTime, +}, .{}); diff --git a/demo/src/app/database/migrations/2024-08-25_13-18-52_hello.zig b/demo/src/app/database/migrations/2024-08-25_13-18-52_hello.zig index 8191eab..0cd1c4e 100644 --- a/demo/src/app/database/migrations/2024-08-25_13-18-52_hello.zig +++ b/demo/src/app/database/migrations/2024-08-25_13-18-52_hello.zig @@ -1,9 +1,9 @@ const jetquery = @import("jetquery"); -pub fn up(repo: *jetquery.Repo) !void { +pub fn up(repo: anytype) !void { _ = repo; } -pub fn down(repo: *jetquery.Repo) !void { +pub fn down(repo: anytype) !void { _ = repo; } diff --git a/demo/src/app/database/migrations/2025-03-10_01-36-58_create_users.zig b/demo/src/app/database/migrations/2025-03-10_01-36-58_create_users.zig new file mode 100644 index 0000000..da76936 --- /dev/null +++ b/demo/src/app/database/migrations/2025-03-10_01-36-58_create_users.zig @@ -0,0 +1,20 @@ +const std = @import("std"); +const jetquery = @import("jetquery"); +const t = jetquery.schema.table; + +pub fn up(repo: anytype) !void { + try repo.createTable( + "users", + &.{ + t.primaryKey("id", .{}), + t.column("email", .string, .{ .unique = true, .index = true }), + t.column("password_hash", .string, .{}), + t.timestamps(.{}), + }, + .{}, + ); +} + +pub fn down(repo: anytype) !void { + try repo.dropTable("users", .{}); +} diff --git a/demo/src/app/views/login.zig b/demo/src/app/views/login.zig new file mode 100644 index 0000000..0736dd2 --- /dev/null +++ b/demo/src/app/views/login.zig @@ -0,0 +1,61 @@ +const std = @import("std"); +const jetzig = @import("jetzig"); +const auth = @import("jetzig").auth; + +pub fn index(request: *jetzig.Request) !jetzig.View { + return request.render(.ok); +} + +pub fn post(request: *jetzig.Request) !jetzig.View { + const Login = struct { + email: []const u8, + password: []const u8, + }; + + const params = try request.expectParams(Login) orelse { + return request.fail(.forbidden); + }; + + // Lookup the user by email + const query = jetzig.database.Query(.User).findBy( + .{ .email = params.email }, + ); + + const user = try request.repo.execute(query) orelse { + return request.fail(.forbidden); + }; + + // Check that the password matches + if (try auth.verifyPassword( + request.allocator, + user.password_hash, + params.password, + )) { + try auth.signIn(request, user.id); + return request.redirect("/", .found); + } + return request.fail(.forbidden); +} + +test "post" { + var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); + defer app.deinit(); + + const hashed_pass = try auth.hashPassword(std.testing.allocator, "test"); + defer std.testing.allocator.free(hashed_pass); + + try jetzig.database.Query(.User).deleteAll().execute(app.repo); + try app.repo.insert(.User, .{ + .id = 1, + .email = "test@test.com", + .password_hash = hashed_pass, + }); + + const response = try app.request(.POST, "/login", .{ + .json = .{ + .email = "test@test.com", + .password = "test", + }, + }); + try response.expectStatus(.found); +} diff --git a/demo/src/app/views/login/index.zmpl b/demo/src/app/views/login/index.zmpl new file mode 100644 index 0000000..605b051 --- /dev/null +++ b/demo/src/app/views/login/index.zmpl @@ -0,0 +1,7 @@ +
+ + + + + +
diff --git a/src/jetzig/auth.zig b/src/jetzig/auth.zig index 457a862..35aa79d 100644 --- a/src/jetzig/auth.zig +++ b/src/jetzig/auth.zig @@ -42,13 +42,16 @@ pub fn verifyPassword( } 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( + var buf: [128]u8 = undefined; + const hash = try std.crypto.pwhash.argon2.strHash( password, .{ .allocator = allocator, .params = .{ .t = 3, .m = 32, .p = 4 }, }, - buf, + &buf, ); + const result = try allocator.alloc(u8, hash.len); + @memcpy(result, hash); + return result; } diff --git a/src/jetzig/testing/App.zig b/src/jetzig/testing/App.zig index 246150b..f3c2a4f 100644 --- a/src/jetzig/testing/App.zig +++ b/src/jetzig/testing/App.zig @@ -78,6 +78,7 @@ pub fn init(allocator: std.mem.Allocator, routes_module: type) !App { /// Free allocated resources for test app. pub fn deinit(self: *App) void { + self.repo.deinit(); self.arena.deinit(); self.allocator.destroy(self.arena); if (self.logger.test_logger.file) |file| file.close();