commit 929f0b23a413ca50fd8c9bfc0eb658882af19b49 Author: yuzu Date: Sat May 3 20:42:44 2025 -0500 first commit 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 0000000..0ccc92b Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/jetzig.png b/public/jetzig.png new file mode 100644 index 0000000..314d70c Binary files /dev/null and b/public/jetzig.png differ diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..1755d47 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,10 @@ +/* Root stylesheet. Load into your Zmpl template with: + * + * + * + */ + +.message { + font-weight: bold; + font-size: 3rem; +} diff --git a/public/zmpl.png b/public/zmpl.png new file mode 100644 index 0000000..0d2f8d3 Binary files /dev/null and b/public/zmpl.png differ 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, .{}); +}