aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2025-11-19 18:58:54 +0100
committerNathan Reiner <nathan@nathanreiner.xyz>2025-11-19 18:58:54 +0100
commit25228df6d13b5e8541672c4cdd84e200ff56a4c4 (patch)
tree924cc6bdc00440c6bf592b04602261cdab17d60a
parent4c06eb64cbed3562e428ce59857d1763098638f3 (diff)
add profile settings to backend and add image loader
-rw-r--r--src/context.zig1
-rw-r--r--src/routes/api/auth/login.zig2
-rw-r--r--src/routes/api/profile/root.zig4
-rw-r--r--src/routes/api/profile/set.zig26
-rw-r--r--src/routes/handler-info.zig9
-rw-r--r--src/storage/user.zig24
-rw-r--r--static/api/profile.js7
-rw-r--r--static/api/rest.js2
-rw-r--r--static/index.js2
-rw-r--r--static/pages/image-viewer/index.css2
-rw-r--r--static/pages/image-viewer/index.js15
-rw-r--r--static/pages/settings/index.js8
-rw-r--r--static/widgets/editable/index.js11
-rw-r--r--static/widgets/image/index.css59
-rw-r--r--static/widgets/image/index.js30
15 files changed, 181 insertions, 21 deletions
diff --git a/src/context.zig b/src/context.zig
index 54ece18..6e73b5d 100644
--- a/src/context.zig
+++ b/src/context.zig
@@ -9,7 +9,6 @@ allocator: std.mem.Allocator,
request: *std.http.Server.Request,
storage: *Storage,
fingerprint: []const u8,
-user: ?[]const u8 = null,
response: struct {
headers: struct {
content_type: []const u8 = "application/json",
diff --git a/src/routes/api/auth/login.zig b/src/routes/api/auth/login.zig
index d7ee5cb..8a33082 100644
--- a/src/routes/api/auth/login.zig
+++ b/src/routes/api/auth/login.zig
@@ -16,7 +16,7 @@ const Result = struct {
pub const access = .everyone;
pub fn post(ctx: *Context, body: Body) anyerror!Result {
- var user = Storage.User.open(ctx.storage, body.user, ctx.allocator) catch return .{
+ var user = Storage.User.open(ctx.storage, body.user) catch return .{
.success = false
};
defer user.deinit();
diff --git a/src/routes/api/profile/root.zig b/src/routes/api/profile/root.zig
index 632c09c..04bf042 100644
--- a/src/routes/api/profile/root.zig
+++ b/src/routes/api/profile/root.zig
@@ -1 +1,5 @@
+const memora = @import("memora");
+const HandlerInfo = memora.routes.HandlerInfo;
+
pub const image = @import("image/root.zig");
+pub const set: HandlerInfo = .from_type(@import("set.zig"));
diff --git a/src/routes/api/profile/set.zig b/src/routes/api/profile/set.zig
new file mode 100644
index 0000000..a007b23
--- /dev/null
+++ b/src/routes/api/profile/set.zig
@@ -0,0 +1,26 @@
+const std = @import("std");
+
+const memora = @import("memora");
+const Context = memora.Context;
+const Storage = memora.Storage;
+
+pub const access = .users;
+
+const Body = struct {
+ full_name: []const u8,
+ birthday: []const u8,
+};
+
+pub fn post(ctx: *Context, body: Body) !void {
+ if (ctx.storage.sessions.get(ctx.storage, ctx.fingerprint)) |session| {
+ var user = try Storage.User.open(ctx.storage, session.info.name);
+ defer user.deinit();
+
+ user.info.full_name = body.full_name;
+ user.info.birthday = body.birthday;
+
+ try user.sync();
+ } else {
+ return error.UnknownSession;
+ }
+}
diff --git a/src/routes/handler-info.zig b/src/routes/handler-info.zig
index 97eb9bd..5183628 100644
--- a/src/routes/handler-info.zig
+++ b/src/routes/handler-info.zig
@@ -51,6 +51,9 @@ pub fn handle(
storage: *Storage,
allocator: std.mem.Allocator,
) !void {
+ const target = try allocator.dupe(u8, request.head.target);
+ defer allocator.free(target);
+
const handler = self.handler_from_method(request.head.method) orelse return request.respond(
"{ \"error\": \"Bad Request\" }",
.{ .status = .bad_request }
@@ -102,7 +105,7 @@ pub fn handle(
error.Forbidden => .{ "{ \"error\": \"Forbidden\" }", .forbidden },
error.NotFound => .{ "{ \"error\": \"Not Found\" }", .not_found },
else => blk: {
- log.err("handler for '{s}' returned {}", .{request.head.target, err});
+ log.err("handler for '{s}' returned {}", .{target, err});
break :blk .{ "{ \"error\": \"Internal Server Error\" }", .internal_server_error };
},
};
@@ -122,7 +125,7 @@ pub fn handle(
if (context.response.headers.fingerprint) |auth_token| {
var value = std.Io.Writer.Allocating.init(arena.allocator());
- try value.writer.print("fingerprint={s}; Secure; Path=/", .{auth_token});
+ try value.writer.print("fingerprint={s}; Path=/", .{auth_token});
try headers.append(allocator, .{
.name = "Set-Cookie",
@@ -184,7 +187,7 @@ fn HandlerWrapper(T: type, name: []const u8) type {
return @call(.auto, @field(T, name), args);
} else if (payload_type == void) {
try @call(.auto, @field(T, name), args);
- return memora.Stream.from_buffer("");
+ return memora.Stream.from_buffer("{}");
} else {
var writer = std.Io.Writer.Allocating.init(ctx.allocator);
var stringify = std.json.Stringify { .writer = &writer.writer };
diff --git a/src/storage/user.zig b/src/storage/user.zig
index 09f7716..86d82ff 100644
--- a/src/storage/user.zig
+++ b/src/storage/user.zig
@@ -48,9 +48,8 @@ arena: std.heap.ArenaAllocator,
pub fn open(
storage: *Storage,
name: []const u8,
- inner_allocator: std.mem.Allocator
) !Self {
- var arena: std.heap.ArenaAllocator = .init(inner_allocator);
+ var arena: std.heap.ArenaAllocator = .init(storage.allocator);
errdefer arena.deinit();
const allocator = arena.allocator();
@@ -125,9 +124,26 @@ pub fn new(
};
}
+pub fn set_password(self: *Self, password: []const u8) !void {
+ const allocator = self.area.allocator();
+ const hash_buf = try allocator.alloc(u8, 256);
+ const hash = try std.crypto.pwhash.bcrypt.strHash(
+ password,
+ .{
+ .params = .{
+ .rounds_log = 3,
+ .silently_truncate_password = false,
+ },
+ .encoding = .phc,
+ },
+ hash_buf
+ );
+
+ self.info.hash = hash;
+}
+
pub fn sync(self: *Self) !void {
- const file = try self.dir.openFile("info.json", .{
- .mode = .write_only,
+ const file = try self.dir.createFile("info.json", .{
.lock = .exclusive,
});
defer file.close();
diff --git a/static/api/profile.js b/static/api/profile.js
index a9ee591..ec633e0 100644
--- a/static/api/profile.js
+++ b/static/api/profile.js
@@ -1 +1,8 @@
import * as rest from './rest.js';
+
+export async function set(name, birthday) {
+ await rest.post('/api/profile/set', {
+ full_name: name,
+ birthday: birthday,
+ });
+}
diff --git a/static/api/rest.js b/static/api/rest.js
index b314615..6b14034 100644
--- a/static/api/rest.js
+++ b/static/api/rest.js
@@ -14,7 +14,7 @@ export function get(url) {
return handle_fetch(fetch(url));
}
-export async function post(url, body) {
+export function post(url, body) {
return handle_fetch(fetch(url, {
method: 'POST',
body: JSON.stringify(body),
diff --git a/static/index.js b/static/index.js
index c84aab9..d5d5c60 100644
--- a/static/index.js
+++ b/static/index.js
@@ -21,7 +21,7 @@ const reload = () => {
image_viewer.clear();
api.images.list().then(images => {
for (const image of images) {
- image_viewer.add(`/api/image/load/${image.id}`);
+ image_viewer.add(image.id);
}
});
}
diff --git a/static/pages/image-viewer/index.css b/static/pages/image-viewer/index.css
index a1a9b76..4b1eb9b 100644
--- a/static/pages/image-viewer/index.css
+++ b/static/pages/image-viewer/index.css
@@ -8,7 +8,7 @@
padding: 10px;
}
-#container img {
+#container sfw-image {
margin: auto;
max-width: 700px;
width: 100%;
diff --git a/static/pages/image-viewer/index.js b/static/pages/image-viewer/index.js
index d28b3e1..0cf8598 100644
--- a/static/pages/image-viewer/index.js
+++ b/static/pages/image-viewer/index.js
@@ -1,6 +1,8 @@
import * as sfw from 'sfw';
const { Div, Img } = sfw.element.native;
+import Image from '../../widgets/image/index.js';
+
const css = await sfw.css(import.meta.url, './index.css')
export default class ImageViewer extends sfw.element.Container {
@@ -9,19 +11,16 @@ export default class ImageViewer extends sfw.element.Container {
constructor() {
super({ css });
+ this.onnewer = () => {}
+ this.onolder = () => {}
+
this.body.append(
this.#container = Div.new({ id: 'container' })
);
}
- add(url) {
- let image;
- this.#container.append(
- image = Img.new({
- className: 'hidden',
- src: url,
- onload: () => image.classList.remove('hidden')
- }));
+ add(id) {
+ this.#container.append(Image.new({ id }))
}
clear() {
diff --git a/static/pages/settings/index.js b/static/pages/settings/index.js
index f316e8e..cc985a5 100644
--- a/static/pages/settings/index.js
+++ b/static/pages/settings/index.js
@@ -54,12 +54,14 @@ export default class SettingsView extends sfw.element.Container {
}),
this.#name = Editable.new({
title: 'Name',
- value: ''
+ value: '',
+ onupdate: () => this.#update(),
}),
this.#birthday = Editable.new({
title: 'Birthday',
type: 'date',
value: '',
+ onupdate: () => this.#update(),
}),
Div.new({
id: 'logout',
@@ -78,6 +80,10 @@ export default class SettingsView extends sfw.element.Container {
)
}
+ #update() {
+ api.profile.set(this.#name.value, this.#birthday.value);
+ }
+
set profile(profile) {
this.#profile = profile;
diff --git a/static/widgets/editable/index.js b/static/widgets/editable/index.js
index 8a72aff..0c93c4e 100644
--- a/static/widgets/editable/index.js
+++ b/static/widgets/editable/index.js
@@ -13,6 +13,8 @@ export default class Editable extends sfw.element.Container {
constructor() {
super({ css });
+ this.onupdate = () => {};
+
this.body.append(
Div.new({
id: 'container',
@@ -24,6 +26,7 @@ export default class Editable extends sfw.element.Container {
if (e.key === 'Enter') {
this.#input.readOnly = true;
this.#update()
+ this.onupdate();
}
},
}),
@@ -33,6 +36,10 @@ export default class Editable extends sfw.element.Container {
onclick: () => {
this.#input.readOnly = !this.#input.readOnly;
this.#update();
+
+ if (this.#input.readOnly) {
+ this.onupdate();
+ }
}
}),
],
@@ -59,6 +66,10 @@ export default class Editable extends sfw.element.Container {
this.#input.value = value;
}
+ get value() {
+ return this.#input.value;
+ }
+
set type(type) {
this.#input.type = type;
}
diff --git a/static/widgets/image/index.css b/static/widgets/image/index.css
new file mode 100644
index 0000000..f4e2dce
--- /dev/null
+++ b/static/widgets/image/index.css
@@ -0,0 +1,59 @@
+#container {
+ position: relative;
+ background: var(--card-background);
+ height: 100%;
+}
+
+#container img {
+ max-width: 700px;
+ width: 100%;
+ border-radius: var(--border-radius);
+ box-shadow: #223223aa 1px 1px 4px;
+ visibility: hidden;
+}
+
+#container.loaded img {
+ visibility: visible;
+}
+
+@keyframes loader {
+}
+
+@keyframes loader {
+ 0% {
+ width: 60px;
+ height: 60px;
+ }
+
+ 25% {
+ border: 5px solid var(--page-background);
+ }
+
+ 50% {
+ width: 80px;
+ height: 80px;
+ }
+
+ 75% {
+ border: 10px solid var(--page-background);
+ }
+
+ 100% {
+ width: 60px;
+ height: 60px;
+ }
+}
+
+#loading {
+ position: absolute;
+ border: 10px solid var(--page-background);
+ border-radius: 100%;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ animation: 0.5s infinite loader ease;
+}
+
+.loaded #loading {
+ display: none;
+}
diff --git a/static/widgets/image/index.js b/static/widgets/image/index.js
new file mode 100644
index 0000000..ad77c4e
--- /dev/null
+++ b/static/widgets/image/index.js
@@ -0,0 +1,30 @@
+import * as sfw from 'sfw';
+const { Div, Img } = sfw.element.native;
+
+const css = await sfw.css(import.meta.url, './index.css')
+
+export default class Image extends sfw.element.Container {
+ #container
+ #image
+
+ constructor() {
+ super ({ css })
+
+ this.body.append(
+ this.#container = Div.new({
+ id: 'container',
+ children: [
+ this.#image = Img.new({
+ onload: () => this.#container.classList.add('loaded'),
+ }),
+ Div.new({ id: 'loading' })
+ ],
+ })
+ );
+ }
+
+ set id(id) {
+ this.#container.classList.remove('loaded');
+ this.#image.src = `/api/image/load/${id}`;
+ }
+}