From 929f0b23a413ca50fd8c9bfc0eb658882af19b49 Mon Sep 17 00:00:00 2001 From: yuzu Date: Sat, 3 May 2025 20:42:44 -0500 Subject: [PATCH] first commit --- .gitignore | 6 + build.zig | 60 +++++ build.zig.zon | 44 ++++ compose.yml | 8 + config/database.zig | 11 + public/favicon.ico | Bin 0 -> 15406 bytes public/jetzig.png | Bin 0 -> 22828 bytes public/styles.css | 10 + public/zmpl.png | Bin 0 -> 8321 bytes src/app/database/Schema.zig | 26 ++ .../2025-05-03_23-41-54_create_sessions.zig | 19 ++ src/app/middleware/AuthMiddleware.zig | 0 src/app/middleware/DemoMiddleware.zig | 82 ++++++ src/app/views/blogs.zig | 169 +++++++++++++ src/app/views/blogs/get.zmpl | 6 + src/app/views/blogs/index.zmpl | 9 + src/app/views/blogs/new.zmpl | 11 + src/app/views/blogs/post.zmpl | 3 + src/app/views/login.zig | 58 +++++ src/app/views/login/index.zmpl | 30 +++ src/app/views/logout.zig | 49 ++++ src/app/views/logout/index.zmpl | 28 +++ src/app/views/root.zig | 64 +++++ src/app/views/root/_article_blob.zmpl | 5 + src/app/views/root/index.zmpl | 32 +++ src/main.zig | 234 ++++++++++++++++++ 26 files changed, 964 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 compose.yml create mode 100644 config/database.zig create mode 100644 public/favicon.ico create mode 100644 public/jetzig.png create mode 100644 public/styles.css create mode 100644 public/zmpl.png create mode 100644 src/app/database/Schema.zig create mode 100644 src/app/database/migrations/2025-05-03_23-41-54_create_sessions.zig create mode 100644 src/app/middleware/AuthMiddleware.zig create mode 100644 src/app/middleware/DemoMiddleware.zig create mode 100644 src/app/views/blogs.zig create mode 100644 src/app/views/blogs/get.zmpl create mode 100644 src/app/views/blogs/index.zmpl create mode 100644 src/app/views/blogs/new.zmpl create mode 100644 src/app/views/blogs/post.zmpl create mode 100644 src/app/views/login.zig create mode 100644 src/app/views/login/index.zmpl create mode 100644 src/app/views/logout.zig create mode 100644 src/app/views/logout/index.zmpl create mode 100644 src/app/views/root.zig create mode 100644 src/app/views/root/_article_blob.zmpl create mode 100644 src/app/views/root/index.zmpl create mode 100644 src/main.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5485280 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +zig-out/ +zig-cache/ +*.core +.jetzig +.zig-cache/ +.env diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..349fa7b --- /dev/null +++ b/build.zig @@ -0,0 +1,60 @@ +const std = @import("std"); +const jetzig = @import("jetzig"); + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "yuzucchii.xyz", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const options = b.addOptions(); + options.addOption([]const u8, "BLOGS_PASSWORD", "admin"); + + exe.root_module.addOptions("dev", options); + + // Example Dependency + // ------------------- + // const iguanas_dep = b.dependency("iguanas", .{ .optimize = optimize, .target = target }); + // exe.root_module.addImport("iguanas", iguanas_dep.module("iguanas")); + // + const uuid = b.dependency("uuid", .{ .optimize = optimize, .target = target }); + exe.root_module.addImport("uuid", uuid.module("uuid")); + // ^ Add all dependencies before `jetzig.jetzigInit()` ^ + + try jetzig.jetzigInit(b, exe, .{}); + + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| run_cmd.addArgs(args); + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const lib_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + + const exe_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_lib_unit_tests.step); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..8d6c246 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,44 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = .yuzucchiidotxyz, + .fingerprint = 0x4831d874912025d4, + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + .jetzig = .{ + .url = "https://github.com/jetzig-framework/jetzig/archive/1feb18fb74e626fe068ec67532318640a9cb83be.tar.gz", + .hash = "jetzig-0.0.0-IpAgLfMzDwDyAqZ05btcLDd9dfE_bxUbfOI_Wx7a19ed", + }, + .uuid = .{ + .url = "git+https://github.com/r4gus/uuid-zig#9d66e23e32cd7208d1becb4fd9352f8a27f89551", + .hash = "uuid-0.3.0-oOieIYF1AAA_BtE7FvVqqTn5uEYTvvz7ycuVnalCOf8C", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..8c7cd31 --- /dev/null +++ b/compose.yml @@ -0,0 +1,8 @@ +services: + postgres: + image: postgres:16 + environment: + POSTGRES_USER: ${JETQUERY_USERNAME} + POSTGRES_PASSWORD: ${JETQUERY_PASSWORD} + ports: + - 5432:5432 diff --git a/config/database.zig b/config/database.zig new file mode 100644 index 0000000..ab4ddc2 --- /dev/null +++ b/config/database.zig @@ -0,0 +1,11 @@ +pub const database = .{ + .development = .{ + .adapter = .postgresql, + }, + .testing = .{ + .adapter = .postgresql, + }, + .production = .{ + .adapter = .postgresql, + }, +}; diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0ccc92bdf940b99f695dec79441bf65ae756c961 GIT binary patch literal 15406 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 YW#MJE`s{S5sRY9RBmZfE|MwR7KW0u{O8@`> literal 0 HcmV?d00001 diff --git a/public/jetzig.png b/public/jetzig.png new file mode 100644 index 0000000000000000000000000000000000000000..314d70c06242be3c69f1c1f5e50d26d3e9eda56a GIT binary patch literal 22828 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 + * + */ + +.message { + font-weight: bold; + font-size: 3rem; +} diff --git a/public/zmpl.png b/public/zmpl.png new file mode 100644 index 0000000000000000000000000000000000000000..0d2f8d356b27194a2665a22da01b4bca57a62bdc GIT binary patch literal 8321 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 Q<^LAQHrt7nR^I&o0f$FC3jhEB literal 0 HcmV?d00001 diff --git a/src/app/database/Schema.zig b/src/app/database/Schema.zig new file mode 100644 index 0000000..eccdcea --- /dev/null +++ b/src/app/database/Schema.zig @@ -0,0 +1,26 @@ +const jetquery = @import("jetzig").jetquery; + +pub const Blog = jetquery.Model( + @This(), + "blogs", + struct { + id: i32, + title: []const u8, + content: ?[]const u8, + created_at: jetquery.DateTime, + updated_at: jetquery.DateTime, + }, + .{}, +); + +pub const Session = jetquery.Model( + @This(), + "sessions", + struct { + id: i32, + session_id: []const u8, + created_at: jetquery.DateTime, + updated_at: jetquery.DateTime, + }, + .{}, +); diff --git a/src/app/database/migrations/2025-05-03_23-41-54_create_sessions.zig b/src/app/database/migrations/2025-05-03_23-41-54_create_sessions.zig new file mode 100644 index 0000000..bfd02a1 --- /dev/null +++ b/src/app/database/migrations/2025-05-03_23-41-54_create_sessions.zig @@ -0,0 +1,19 @@ +const std = @import("std"); +const jetquery = @import("jetquery"); +const t = jetquery.schema.table; + +pub fn up(repo: anytype) !void { + try repo.createTable( + "sessions", + &.{ + t.primaryKey("id", .{}), + t.column("session_id", .string, .{}), + t.timestamps(.{}), + }, + .{}, + ); +} + +pub fn down(repo: anytype) !void { + try repo.dropTable("sessions", .{}); +} diff --git a/src/app/middleware/AuthMiddleware.zig b/src/app/middleware/AuthMiddleware.zig new file mode 100644 index 0000000..e69de29 diff --git a/src/app/middleware/DemoMiddleware.zig b/src/app/middleware/DemoMiddleware.zig new file mode 100644 index 0000000..adc6f9d --- /dev/null +++ b/src/app/middleware/DemoMiddleware.zig @@ -0,0 +1,82 @@ +/// Demo middleware. Assign middleware by declaring `pub const middleware` in the +/// `jetzig_options` defined in your application's `src/main.zig`. +/// +/// Middleware is called before and after the request, providing full access to the active +/// request, allowing you to execute any custom code for logging, tracking, inserting response +/// headers, etc. +/// +/// This middleware is configured in the demo app's `src/main.zig`: +/// +/// ``` +/// pub const jetzig_options = struct { +/// pub const middleware: []const type = &.{@import("app/middleware/DemoMiddleware.zig")}; +/// }; +/// ``` +const std = @import("std"); +const jetzig = @import("jetzig"); + +const Zmd = @import("zmd").Zmd; +const fragments = @import("zmd").html.DefaultFragments; + +/// Define any custom data fields you want to store here. Assigning to these fields in the `init` +/// function allows you to access them in various middleware callbacks defined below, where they +/// can also be modified. +my_custom_value: []const u8, + +const DemoMiddleware = @This(); + +/// Initialize middleware. +pub fn init(request: *jetzig.http.Request) !*DemoMiddleware { + var middleware = try request.allocator.create(DemoMiddleware); + middleware.my_custom_value = "initial value"; + return middleware; +} + +/// 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: *DemoMiddleware, request: *jetzig.http.Request) !void { + // Middleware can invoke `request.redirect` or `request.render`. All request processing stops + // and the response is immediately returned if either of these two functions are called + // during middleware processing. + // _ = request.redirect("/foobar", .moved_permanently); + // _ = request.render(.unauthorized); + + try request.server.logger.DEBUG( + "[DemoMiddleware:afterRequest] my_custom_value: {s}", + .{self.my_custom_value}, + ); + self.my_custom_value = @tagName(request.method); +} + +/// Invoked immediately before the response renders to the client. +/// The response can be modified here if needed. +pub fn beforeResponse( + self: *DemoMiddleware, + request: *jetzig.http.Request, + response: *jetzig.http.Response, +) !void { + try request.server.logger.DEBUG( + "[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}", + .{ self.my_custom_value, @tagName(response.status_code) }, + ); +} + +/// 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: *DemoMiddleware, + request: *jetzig.http.Request, + response: *jetzig.http.Response, +) !void { + _ = self; + _ = response; + try request.server.logger.DEBUG("[DemoMiddleware:afterResponse] response completed", .{}); +} + +/// 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 +/// freed before the next request starts processing. +pub fn deinit(self: *DemoMiddleware, request: *jetzig.http.Request) void { + request.allocator.destroy(self); +} diff --git a/src/app/views/blogs.zig b/src/app/views/blogs.zig new file mode 100644 index 0000000..ca6ba35 --- /dev/null +++ b/src/app/views/blogs.zig @@ -0,0 +1,169 @@ +const std = @import("std"); +const jetzig = @import("jetzig"); + +const Zmd = @import("zmd").Zmd; +const tokens = @import("zmd").tokens; +const Node = @import("zmd").Node; + +/// Default fragments. Pass this to `Zmd.toHtml` or provide your own. +/// Formatters can be functions receiving an allocator, the current node, and the rendered +/// content, or a 2-element tuple containing the open and close for each node. +pub const Fragments = struct { + pub fn root(allocator: std.mem.Allocator, node: Node) ![]const u8 { + return try std.fmt.allocPrint(allocator, "{s}", .{node.content}); + } + + pub fn block(allocator: std.mem.Allocator, node: Node) ![]const u8 { + const style = "font-family: Monospace;"; + + return if (node.meta) |meta| + std.fmt.allocPrint(allocator, + \\
{s}
+ , .{ meta, style, node.content }) + else + std.fmt.allocPrint(allocator, + \\
{s}
+ , .{ style, node.content }); + } + + pub fn link(allocator: std.mem.Allocator, node: Node) ![]const u8 { + return std.fmt.allocPrint(allocator, + \\{s} + , .{ node.href.?, node.title.? }); + } + + pub fn image(allocator: std.mem.Allocator, node: Node) ![]const u8 { + return std.fmt.allocPrint(allocator, + \\ + , .{ node.href.?, node.title.? }); + } + + pub const h1 = .{ "

", "

\n" }; + pub const h2 = .{ "

", "

\n" }; + pub const h3 = .{ "

", "

\n" }; + pub const h4 = .{ "

", "

\n" }; + pub const h5 = .{ "
", "
\n" }; + pub const h6 = .{ "
", "
\n" }; + pub const bold = .{ "", "" }; + pub const italic = .{ "", "" }; + pub const unordered_list = .{ "
    ", "
" }; + pub const ordered_list = .{ "
    ", "
" }; + pub const list_item = .{ "
  • ", "
  • " }; + pub const code = .{ "", "" }; + pub const paragraph = .{ "\n

    ", "

    \n" }; + pub const default = .{ "", "" }; +}; + +/// Escape HTML entities. +pub fn escape(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { + const replacements = .{ + .{ "&", "&" }, + .{ "<", "<" }, + .{ ">", ">" }, + }; + + var output = input; + inline for (replacements) |replacement| { + output = try std.mem.replaceOwned(u8, allocator, output, replacement[0], replacement[1]); + } + return output; +} + +pub fn index(request: *jetzig.Request) !jetzig.View { + const query = jetzig.database.Query(.Blog).orderBy(.{.created_at = .desc}); + const blogs = try request.repo.all(query); + + var root = try request.data(.object); + try root.put("blogs", blogs); + + return request.render(.ok); +} + +pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View { + const query = jetzig.database.Query(.Blog).find(id); + const blog = try request.repo.execute(query) orelse return request.fail(.not_found); + + const Blog = struct { + id: i32, + title: []const u8, + content: ?[]const u8, + created_at: jetzig.jetquery.DateTime, + updated_at: jetzig.jetquery.DateTime, + }; + + var zmd = Zmd.init(request.allocator); + defer zmd.deinit(); + + try zmd.parse(blog.content orelse { + return request.fail(.unprocessable_entity); + }); + + const blob = try zmd.toHtml(Fragments); + + const eb = Blog{ + .id = blog.id, + .title = blog.title, + .content = blob, + .created_at = blog.created_at, + .updated_at = blog.updated_at, + }; + + var root = try request.data(.object); + try root.put("blog", eb); + + try request.process(); + return request.render(.ok); +} + +pub fn new(request: *jetzig.Request) !jetzig.View { + return request.render(.ok); +} + +pub fn post(request: *jetzig.Request) !jetzig.View { + const params = try request.params(); + + const title = params.getT(.string, "title") orelse { + return request.fail(.unprocessable_entity); + }; + + const content = params.getT(.string, "content") orelse { + return request.fail(.unprocessable_entity); + }; + + try request.repo.insert(.Blog, .{ .title = title, .content = content }); + + return request.redirect("/blogs", .moved_permanently); +} + + +test "index" { + var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); + defer app.deinit(); + + const response = try app.request(.GET, "/blogs", .{}); + try response.expectStatus(.ok); +} + +test "get" { + var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); + defer app.deinit(); + + const response = try app.request(.GET, "/blogs/example-id", .{}); + try response.expectStatus(.ok); +} + +test "new" { + var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); + defer app.deinit(); + + const response = try app.request(.GET, "/blogs/new", .{}); + try response.expectStatus(.ok); +} + +test "post" { + var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); + defer app.deinit(); + + const response = try app.request(.POST, "/blogs", .{}); + try response.expectStatus(.created); +} diff --git a/src/app/views/blogs/get.zmpl b/src/app/views/blogs/get.zmpl new file mode 100644 index 0000000..b935d03 --- /dev/null +++ b/src/app/views/blogs/get.zmpl @@ -0,0 +1,6 @@ +
    +

    {{.blog.title}}

    +
    + {{zmpl.fmt.raw(zmpl.ref("blog.content"))}} +
    +
    diff --git a/src/app/views/blogs/index.zmpl b/src/app/views/blogs/index.zmpl new file mode 100644 index 0000000..470a07c --- /dev/null +++ b/src/app/views/blogs/index.zmpl @@ -0,0 +1,9 @@ +
    + @for (.blogs) |blog| { + {{blog.title}} + {{zmpl.fmt.datetime(blog.get("created_at"), "%Y-%m-%d %H:%M")}} +
    + } +
    + New Blog +
    diff --git a/src/app/views/blogs/new.zmpl b/src/app/views/blogs/new.zmpl new file mode 100644 index 0000000..62adba1 --- /dev/null +++ b/src/app/views/blogs/new.zmpl @@ -0,0 +1,11 @@ +
    +
    + + + + + + + +
    +
    diff --git a/src/app/views/blogs/post.zmpl b/src/app/views/blogs/post.zmpl new file mode 100644 index 0000000..76457d0 --- /dev/null +++ b/src/app/views/blogs/post.zmpl @@ -0,0 +1,3 @@ +
    + Content goes here +
    diff --git a/src/app/views/login.zig b/src/app/views/login.zig new file mode 100644 index 0000000..eb3b03a --- /dev/null +++ b/src/app/views/login.zig @@ -0,0 +1,58 @@ +const std = @import("std"); +const jetzig = @import("jetzig"); +const uuid4 = @import("uuid").v4; + +pub fn index(request: *jetzig.Request) !jetzig.View { + // check if logged in + const cookies = try request.cookies(); + + if (cookies.get("session")) |session| if (session.value.len != 0) + return request.redirect("/blogs", .moved_permanently); + + return request.render(.ok); +} + +pub fn post(request: *jetzig.Request) !jetzig.View { + // ask for password + const cookies = try request.cookies(); + + const env_map = try request.allocator.create(std.process.EnvMap); + env_map.* = try std.process.getEnvMap(request.allocator); + defer env_map.deinit(); + + const secrets = @import("dev").BLOGS_PASSWORD; + + std.debug.print("body data: {s}\n", .{request.body}); + + const login_data = std.json.parseFromSliceLeaky(struct { + password: []const u8, + }, request.allocator, request.body, .{}) catch { + return request.fail(.bad_request); + }; + + var buf: [0x100]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buf); + const allocator = fba.allocator(); + + if (std.mem.eql(u8, login_data.password, secrets)) { + // logged in, creating cookie + const uuid = try std.fmt.allocPrint(allocator, "{d}", .{uuid4.new()}); + + try cookies.put(.{ + .name = "session", + .value = uuid, + .path = "/", + .http_only = true, + .secure = false, + .max_age = 60 * 60 * 24 * 7, // 1 week + }); + + // post to Session table + + try request.repo.insert(.Session, .{ .session_id = uuid }); + + return request.render(.created); + } else { + return request.fail(.unauthorized); + } +} diff --git a/src/app/views/login/index.zmpl b/src/app/views/login/index.zmpl new file mode 100644 index 0000000..d062d89 --- /dev/null +++ b/src/app/views/login/index.zmpl @@ -0,0 +1,30 @@ +
    +
    + + + +
    + + +
    + diff --git a/src/app/views/logout.zig b/src/app/views/logout.zig new file mode 100644 index 0000000..bb3bb8e --- /dev/null +++ b/src/app/views/logout.zig @@ -0,0 +1,49 @@ +const std = @import("std"); +const jetzig = @import("jetzig"); + +pub fn index(request: *jetzig.Request) !jetzig.View { + return request.render(.ok); +} + +pub fn post(request: *jetzig.Request) !jetzig.View { + // delete entry + const cookies = try request.cookies(); + const session = cookies.get("session") orelse { + return request.redirect("/login", .moved_permanently); + }; + + const query = jetzig.database.Query(.Session) + .findBy(.{ .session_id = session.value }); + + _ = request.repo.execute(query) catch { + return request.redirect("/login", .moved_permanently); + }; + + // delete it + const delete_query = jetzig.database.Query(.Session) + .delete() + .where(.{ .{ .session_id, .eql, session.value } }); + + _ = request.repo.execute(delete_query) catch { + return request.fail(.internal_server_error); + }; + + // delete cookie + try cookies.put(.{ + .name = "session", + .value = "", + .path = "/", + .max_age = 0, + .http_only = true, + .secure = false, + .expires = 0, + }); + + try request.headers.append("Set-Cookie", + "session=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax"); + + std.debug.print("Deleting session cookie at path: {?s}\n", .{session.path}); + + return request.render(.ok); +} + diff --git a/src/app/views/logout/index.zmpl b/src/app/views/logout/index.zmpl new file mode 100644 index 0000000..69d72bc --- /dev/null +++ b/src/app/views/logout/index.zmpl @@ -0,0 +1,28 @@ + + + + diff --git a/src/app/views/root.zig b/src/app/views/root.zig new file mode 100644 index 0000000..469b301 --- /dev/null +++ b/src/app/views/root.zig @@ -0,0 +1,64 @@ +const std = @import("std"); +const jetzig = @import("jetzig"); + +const Article = struct { + title: [255]u8, + blob: [255]u8 +}; + +/// `src/app/views/root.zig` represents the root URL `/` +/// The `index` view function is invoked when when the HTTP verb is `GET`. +/// Other view types are invoked either by passing a resource ID value (e.g. `/1234`) or by using +/// a different HTTP verb: +/// +/// GET / => index(request, data) +/// GET /1234 => get(id, request, data) +/// POST / => post(request, data) +/// PUT /1234 => put(id, request, data) +/// PATCH /1234 => patch(id, request, data) +/// DELETE /1234 => delete(id, request, data) +pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { + // The first call to `data.object()` or `data.array()` sets the root response data value. + // JSON requests return a JSON string representation of the root data value. + // Zmpl templates can access all values in the root data value. + var root = try data.object(); + + // Add a string to the root object. + try root.put("title", data.string("yuzucchii.xyz")); + + // Request params have the same type as a `data.object()` so they can be inserted them + // directly into the response data. Fetch `http://localhost:8080/?message=hello` to set the + // param. JSON data is also accepted when the `content-type: application/json` header is + // present. + const params = try request.params(); + + var obj = try data.object(); + try obj.put("title",data.string("The simplest article ever")); + try obj.put("description",data.string("Hello in this article I being to do things")); + + var objj = try data.object(); + try objj.put("title",data.string("The most complicated thing ever")); + try objj.put("description",data.string("Dude, that shit is like harder than a rock")); + + var array = try root.put("articles", .array); + try array.append(obj); + try array.append(objj); + + const query = jetzig.database.Query(.Blog).orderBy(.{.created_at = .desc}); + const blogs = try request.repo.all(query); + + try root.put("articles", blogs); + + try root.put("message_param", params.get("message")); + + // Set arbitrary response headers as required. `content-type` is automatically assigned for + // HTML, JSON responses. + // + // Static files located in `public/` in the root of your project directory are accessible + // from the root path (e.g. `public/jetzig.png`) is available at `/jetzig.png` and the + // content type is inferred from the extension using MIME types. + try request.response.headers.append("x-example-header", "example header value"); + + // Render the response and set the response code. + return request.render(.ok); +} diff --git a/src/app/views/root/_article_blob.zmpl b/src/app/views/root/_article_blob.zmpl new file mode 100644 index 0000000..dab85aa --- /dev/null +++ b/src/app/views/root/_article_blob.zmpl @@ -0,0 +1,5 @@ +@args title: []const u8, blob: []const u8 +
    +

    {{title}}

    +

    {{blob}}

    +
    diff --git a/src/app/views/root/index.zmpl b/src/app/views/root/index.zmpl new file mode 100644 index 0000000..ce5b7b9 --- /dev/null +++ b/src/app/views/root/index.zmpl @@ -0,0 +1,32 @@ +@args articles: Zmpl.Array + + + + + + + + + {{.title}} + + +
    +

    Hello, I like doing things.

    +

    I created this website using Jetzig in order to store things I do.

    + +
    + Blog + TTRPG + Design + Writing +
    + + @partial root/article_blob(title: "hi test", blob: "owo") + + @for (.articles) |article| { + @partial root/article_blob(title: article.title, blob: article.content) + } +
    + + diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..0b9402e --- /dev/null +++ b/src/main.zig @@ -0,0 +1,234 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const jetzig = @import("jetzig"); +const zmd = @import("zmd"); + +pub const routes = @import("routes"); +pub const static = @import("static"); + +// Override default settings in `jetzig.config` here: +pub const jetzig_options = struct { + /// Middleware chain. Add any custom middleware here, or use middleware provided in + /// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`). + pub const middleware: []const type = &.{ + // jetzig.middleware.AuthMiddleware, + // jetzig.middleware.AntiCsrfMiddleware, + // jetzig.middleware.HtmxMiddleware, + // jetzig.middleware.CompressionMiddleware, + // @import("app/middleware/DemoMiddleware.zig"), + }; + + // Maximum bytes to allow in request body. + pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16); + + // Maximum filesize for `public/` content. + pub const max_bytes_public_content: usize = std.math.pow(usize, 2, 20); + + // Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`). + pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18); + + // Maximum length of a header name. There is no limit imposed by the HTTP specification but + // AWS load balancers reference 40 as a limit so we use that as a baseline: + // https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_HttpHeaderConditionConfig.html + // This can be increased if needed. + pub const max_bytes_header_name: u16 = 40; + + /// Maximum number of `multipart/form-data`-encoded fields to accept per request. + pub const max_multipart_form_fields: usize = 20; + + // Log message buffer size. Log messages exceeding this size spill to heap with degraded + // performance. Log messages should aim to fit in the message buffer. + pub const log_message_buffer_len: usize = 4096; + + // Maximum log pool size. When a log buffer is no longer required it is returned to a pool + // for recycling. When logging i/o is slow, a high volume of requests will result in this + // pool growing. When the pool size reaches the maximum value defined here, log events are + // freed instead of recycled. + pub const max_log_pool_len: usize = 256; + + // Number of request threads. Defaults to number of detected CPUs. + pub const thread_count: ?u16 = null; + + // Number of response worker threads. + pub const worker_count: u16 = 4; + + // Total number of connections managed by worker threads. + pub const max_connections: u16 = 512; + + // Per-thread stack memory to use before spilling into request arena (possibly with allocations). + pub const buffer_size: usize = 64 * 1024; + + // The size of each item in the available memory pool used by requests for rendering. + // Total retained allocation: `worker_count * max_connections`. + pub const arena_size: usize = 1024 * 1024; + + // Path relative to cwd() to serve public content from. Symlinks are not followed. + pub const public_content_path = "public"; + + // HTTP buffer. Must be large enough to store all headers. This should typically not be modified. + pub const http_buffer_size: usize = std.math.pow(usize, 2, 16); + + // The number of worker threads to spawn on startup for processing Jobs (NOT the number of + // HTTP server worker threads). + pub const job_worker_threads: usize = 4; + + // Duration before looking for more Jobs when the queue is found to be empty, in + // milliseconds. + pub const job_worker_sleep_interval_ms: usize = 10; + + /// Database Schema. Set to `@import("Schema")` to load `src/app/database/Schema.zig`. + pub const Schema = @import("Schema"); + + /// HTTP cookie configuration + pub const cookies: jetzig.http.Cookies.CookieOptions = switch (jetzig.environment) { + .development, .testing => .{ + .domain = "localhost", + .path = "/", + }, + .production => .{ + .same_site = .strict, + .secure = true, + .http_only = true, + .path = "/", + }, + }; + + /// Key-value store options. + /// Available backends: + /// * memory: Simple, in-memory hashmap-backed store. + /// * file: Rudimentary file-backed store. + /// * valkey: Valkey-backed store with connection pool. + /// + /// When using `.file` or `.valkey` backend, you must also set `.file_options` or + /// `.valkey_options` respectively. + /// + /// ## File backend: + // .backend = .file, + // .file_options = .{ + // .path = "/path/to/jetkv-store.db", + // .truncate = false, // Set to `true` to clear the store on each server launch. + // .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096), + // }, + // + // ## Valkey backend + // .backend = .valkey, + // .valkey_options = .{ + // .host = "localhost", + // .port = 6379, + // .timeout = 1000, // in milliseconds, i.e. 1 second. + // .connect = .lazy, // Connect on first use, or `auto` to connect on server startup. + // .buffer_size = 8192, + // .pool_size = 8, + // }, + /// Available configuration options for `store`, `job_queue`, and `cache` are identical. + /// + /// For production deployment, the `valkey` backend is recommended for all use cases. + /// + /// The general-purpose key-value store is exposed as `request.store` in views and is also + /// available in as `env.store` in all jobs/mailers. + pub const store: jetzig.kv.Store.Options = .{ + .backend = .memory, + }; + + /// Job queue options. Identical to `store` options, but allows using different + /// backends (e.g. `.memory` for key-value store, `.file` for jobs queue. + /// The job queue is managed internally by Jetzig. + pub const job_queue: jetzig.kv.Store.Options = .{ + .backend = .memory, + }; + + /// Cache options. Identical to `store` options, but allows using different + /// backends (e.g. `.memory` for key-value store, `.file` for cache. + pub const cache: jetzig.kv.Store.Options = .{ + .backend = .memory, + }; + + /// SMTP configuration for Jetzig Mail. It is recommended to use a local SMTP relay, + /// e.g.: https://github.com/juanluisbaptiste/docker-postfix + /// + /// Each configuration option can be overridden with environment variables: + /// `JETZIG_SMTP_PORT` + /// `JETZIG_SMTP_ENCRYPTION` + /// `JETZIG_SMTP_HOST` + /// `JETZIG_SMTP_USERNAME` + /// `JETZIG_SMTP_PASSWORD` + // pub const smtp: jetzig.mail.SMTPConfig = .{ + // .port = 25, + // .encryption = .none, // .insecure, .none, .tls, .start_tls + // .host = "localhost", + // .username = null, + // .password = null, + // }; + + /// Force email delivery in development mode (instead of printing email body to logger). + pub const force_development_email_delivery = false; + + // Set custom fragments for rendering markdown templates. Any values will fall back to + // defaults provided by Zmd (https://github.com/jetzig-framework/zmd/blob/main/src/zmd/html.zig). + pub const markdown_fragments = struct { + pub const root = .{ + "
    ", + "
    ", + }; + pub const h1 = .{ + "

    ", + "

    ", + }; + pub const h2 = .{ + "

    ", + "

    ", + }; + pub const h3 = .{ + "

    ", + "

    ", + }; + pub const paragraph = .{ + "

    ", + "

    ", + }; + pub const code = .{ + "", + "", + }; + + pub const unordered_list = .{ + "
      ", + "
    ", + }; + + pub const ordered_list = .{ + "
      ", + "
    ", + }; + + pub fn block(allocator: std.mem.Allocator, node: zmd.Node) ![]const u8 { + return try std.fmt.allocPrint(allocator, + \\
    {s}
    + , .{ node.meta, node.content }); + } + + pub fn link(allocator: std.mem.Allocator, node: zmd.Node) ![]const u8 { + return try std.fmt.allocPrint(allocator, + \\{1s} + , .{ node.href.?, node.title.? }); + } + }; +}; + +pub fn init(app: *jetzig.App) !void { + _ = app; + // Example custom route: + // app.route(.GET, "/custom/:id/foo/bar", @import("app/views/custom/foo.zig"), .bar); +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator; + defer if (builtin.mode == .Debug) std.debug.assert(gpa.deinit() == .ok); + + var app = try jetzig.init(allocator); + defer app.deinit(); + + try app.start(routes, .{}); +}