first commit
This commit is contained in:
commit
929f0b23a4
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
zig-out/
|
||||
zig-cache/
|
||||
*.core
|
||||
.jetzig
|
||||
.zig-cache/
|
||||
.env
|
60
build.zig
Normal file
60
build.zig
Normal file
@ -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);
|
||||
}
|
44
build.zig.zon
Normal file
44
build.zig.zon
Normal file
@ -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 <url>`, 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",
|
||||
},
|
||||
}
|
8
compose.yml
Normal file
8
compose.yml
Normal file
@ -0,0 +1,8 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_USER: ${JETQUERY_USERNAME}
|
||||
POSTGRES_PASSWORD: ${JETQUERY_PASSWORD}
|
||||
ports:
|
||||
- 5432:5432
|
11
config/database.zig
Normal file
11
config/database.zig
Normal file
@ -0,0 +1,11 @@
|
||||
pub const database = .{
|
||||
.development = .{
|
||||
.adapter = .postgresql,
|
||||
},
|
||||
.testing = .{
|
||||
.adapter = .postgresql,
|
||||
},
|
||||
.production = .{
|
||||
.adapter = .postgresql,
|
||||
},
|
||||
};
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
public/jetzig.png
Normal file
BIN
public/jetzig.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
10
public/styles.css
Normal file
10
public/styles.css
Normal file
@ -0,0 +1,10 @@
|
||||
/* Root stylesheet. Load into your Zmpl template with:
|
||||
*
|
||||
* <link rel="stylesheet" href="/styles.css" />
|
||||
*
|
||||
*/
|
||||
|
||||
.message {
|
||||
font-weight: bold;
|
||||
font-size: 3rem;
|
||||
}
|
BIN
public/zmpl.png
Normal file
BIN
public/zmpl.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
26
src/app/database/Schema.zig
Normal file
26
src/app/database/Schema.zig
Normal file
@ -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,
|
||||
},
|
||||
.{},
|
||||
);
|
@ -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", .{});
|
||||
}
|
0
src/app/middleware/AuthMiddleware.zig
Normal file
0
src/app/middleware/AuthMiddleware.zig
Normal file
82
src/app/middleware/DemoMiddleware.zig
Normal file
82
src/app/middleware/DemoMiddleware.zig
Normal file
@ -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);
|
||||
}
|
169
src/app/views/blogs.zig
Normal file
169
src/app/views/blogs.zig
Normal file
@ -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,
|
||||
\\<pre class="language-{s}" style="{s}"><code>{s}</code></pre>
|
||||
, .{ meta, style, node.content })
|
||||
else
|
||||
std.fmt.allocPrint(allocator,
|
||||
\\<pre style="{s}"><code>{s}</code></pre>
|
||||
, .{ style, node.content });
|
||||
}
|
||||
|
||||
pub fn link(allocator: std.mem.Allocator, node: Node) ![]const u8 {
|
||||
return std.fmt.allocPrint(allocator,
|
||||
\\<a href="{s}">{s}</a>
|
||||
, .{ node.href.?, node.title.? });
|
||||
}
|
||||
|
||||
pub fn image(allocator: std.mem.Allocator, node: Node) ![]const u8 {
|
||||
return std.fmt.allocPrint(allocator,
|
||||
\\<img src="{s}" title="{s}" />
|
||||
, .{ node.href.?, node.title.? });
|
||||
}
|
||||
|
||||
pub const h1 = .{ "<h1>", "</h1>\n" };
|
||||
pub const h2 = .{ "<h2>", "</h2>\n" };
|
||||
pub const h3 = .{ "<h3>", "</h3>\n" };
|
||||
pub const h4 = .{ "<h4>", "</h4>\n" };
|
||||
pub const h5 = .{ "<h5>", "</h5>\n" };
|
||||
pub const h6 = .{ "<h6>", "</h6>\n" };
|
||||
pub const bold = .{ "<b>", "</b>" };
|
||||
pub const italic = .{ "<i>", "</i>" };
|
||||
pub const unordered_list = .{ "<ul>", "</ul>" };
|
||||
pub const ordered_list = .{ "<ol>", "</ol>" };
|
||||
pub const list_item = .{ "<li>", "</li>" };
|
||||
pub const code = .{ "<span style=\"font-family: Monospace\">", "</span>" };
|
||||
pub const paragraph = .{ "\n<p>", "</p>\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);
|
||||
}
|
6
src/app/views/blogs/get.zmpl
Normal file
6
src/app/views/blogs/get.zmpl
Normal file
@ -0,0 +1,6 @@
|
||||
<div>
|
||||
<h1>{{.blog.title}}</h1>
|
||||
<div class="mt-6 prose prose-lg dark:prose-dark">
|
||||
{{zmpl.fmt.raw(zmpl.ref("blog.content"))}}
|
||||
</div>
|
||||
</div>
|
9
src/app/views/blogs/index.zmpl
Normal file
9
src/app/views/blogs/index.zmpl
Normal file
@ -0,0 +1,9 @@
|
||||
<div>
|
||||
@for (.blogs) |blog| {
|
||||
<a href="/blogs/{{blog.id}}">{{blog.title}}</a>
|
||||
{{zmpl.fmt.datetime(blog.get("created_at"), "%Y-%m-%d %H:%M")}}
|
||||
<br/>
|
||||
}
|
||||
<hr/>
|
||||
<a href="/blogs/new">New Blog</a>
|
||||
</div>
|
11
src/app/views/blogs/new.zmpl
Normal file
11
src/app/views/blogs/new.zmpl
Normal file
@ -0,0 +1,11 @@
|
||||
<div>
|
||||
<form action="/blogs" method="POST">
|
||||
<label>Title</label>
|
||||
<input name="title" />
|
||||
|
||||
<label>Content</label>
|
||||
<textarea name="content"></textarea>
|
||||
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</div>
|
3
src/app/views/blogs/post.zmpl
Normal file
3
src/app/views/blogs/post.zmpl
Normal file
@ -0,0 +1,3 @@
|
||||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
58
src/app/views/login.zig
Normal file
58
src/app/views/login.zig
Normal file
@ -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);
|
||||
}
|
||||
}
|
30
src/app/views/login/index.zmpl
Normal file
30
src/app/views/login/index.zmpl
Normal file
@ -0,0 +1,30 @@
|
||||
<div>
|
||||
<form id="login-form">
|
||||
<label>Discrete password</label>
|
||||
<input name="password" type="password"/>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.querySelector('#login-form').addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
const password = document.querySelector('input[name="password"]').value;
|
||||
|
||||
fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ password })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
window.location.href = '/blogs';
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Login error:', err);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
49
src/app/views/logout.zig
Normal file
49
src/app/views/logout.zig
Normal file
@ -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);
|
||||
}
|
||||
|
28
src/app/views/logout/index.zmpl
Normal file
28
src/app/views/logout/index.zmpl
Normal file
@ -0,0 +1,28 @@
|
||||
<button id="logout-button">Logout</button>
|
||||
|
||||
<script>
|
||||
document.getElementById('logout-button').addEventListener('click', function () {
|
||||
fetch('/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ action: 'logout' })
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Logout failed: ${response.status}`);
|
||||
}
|
||||
return response.json(); // if you expect JSON back
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Logout successful:', data);
|
||||
// window.location.href = '/login'; // redirect if needed
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during logout:', error);
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
64
src/app/views/root.zig
Normal file
64
src/app/views/root.zig
Normal file
@ -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);
|
||||
}
|
5
src/app/views/root/_article_blob.zmpl
Normal file
5
src/app/views/root/_article_blob.zmpl
Normal file
@ -0,0 +1,5 @@
|
||||
@args title: []const u8, blob: []const u8
|
||||
<div class="mb-4">
|
||||
<h2 class="text-2xl font-semibold mb-2">{{title}}</h2>
|
||||
<p class="text-lg">{{blob}}</p>
|
||||
</div>
|
32
src/app/views/root/index.zmpl
Normal file
32
src/app/views/root/index.zmpl
Normal file
@ -0,0 +1,32 @@
|
||||
@args articles: Zmpl.Array
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
<title> {{.title}} </title
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="mx-auto max-w-3xl mt-sm">
|
||||
<h1 class="text-3xl font-bold mb-4">Hello, I like doing things.</h1>
|
||||
<p class="text-lg mb-6">I created this website using Jetzig in order to store things I do.</p>
|
||||
|
||||
<div class="mb-8">
|
||||
<a href="/blogs" class="text-blue-500 hover:underline">Blog</a>
|
||||
<a href="#" class="text-blue-500 hover:underline ml-4">TTRPG</a>
|
||||
<a href="#" class="text-blue-500 hover:underline ml-4">Design</a>
|
||||
<a href="#" class="text-blue-500 hover:underline ml-4">Writing</a>
|
||||
</div>
|
||||
|
||||
@partial root/article_blob(title: "hi test", blob: "owo")
|
||||
|
||||
@for (.articles) |article| {
|
||||
@partial root/article_blob(title: article.title, blob: article.content)
|
||||
}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
234
src/main.zig
Normal file
234
src/main.zig
Normal file
@ -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 = .{
|
||||
"<div class='p-5'>",
|
||||
"</div>",
|
||||
};
|
||||
pub const h1 = .{
|
||||
"<h1 class='text-2xl mb-3 text-green font-bold'>",
|
||||
"</h1>",
|
||||
};
|
||||
pub const h2 = .{
|
||||
"<h2 class='text-xl mb-3 font-bold'>",
|
||||
"</h2>",
|
||||
};
|
||||
pub const h3 = .{
|
||||
"<h3 class='text-lg mb-3 font-bold'>",
|
||||
"</h3>",
|
||||
};
|
||||
pub const paragraph = .{
|
||||
"<p class='p-3'>",
|
||||
"</p>",
|
||||
};
|
||||
pub const code = .{
|
||||
"<span class='font-mono bg-gray-900 p-2 text-white'>",
|
||||
"</span>",
|
||||
};
|
||||
|
||||
pub const unordered_list = .{
|
||||
"<ul class='list-disc ms-8 leading-8'>",
|
||||
"</ul>",
|
||||
};
|
||||
|
||||
pub const ordered_list = .{
|
||||
"<ul class='list-decimal ms-8 leading-8'>",
|
||||
"</ul>",
|
||||
};
|
||||
|
||||
pub fn block(allocator: std.mem.Allocator, node: zmd.Node) ![]const u8 {
|
||||
return try std.fmt.allocPrint(allocator,
|
||||
\\<pre class="w-1/2 font-mono mt-4 ms-3 bg-gray-900 p-2 text-white"><code class="language-{?s}">{s}</code></pre>
|
||||
, .{ node.meta, node.content });
|
||||
}
|
||||
|
||||
pub fn link(allocator: std.mem.Allocator, node: zmd.Node) ![]const u8 {
|
||||
return try std.fmt.allocPrint(allocator,
|
||||
\\<a class="underline decoration-sky-500" href="{0s}" title={1s}>{1s}</a>
|
||||
, .{ 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, .{});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user