From 4b6e37397d3a9a80db0c20484b712175c7b9c9c7 Mon Sep 17 00:00:00 2001 From: Nathan Reiner Date: Mon, 24 Nov 2025 15:55:20 +0100 Subject: add password-dialog --- src/routes/api/profile/root.zig | 1 + src/routes/api/profile/update-password.zig | 37 ++++++++++++++++++ src/storage/user.zig | 2 +- static/api/profile.js | 7 ++++ static/pages/settings/index.css | 13 +++++++ static/pages/settings/index.js | 10 +++++ static/widgets/password-dialog/index.css | 51 +++++++++++++++++++++++++ static/widgets/password-dialog/index.js | 60 ++++++++++++++++++++++++++++++ 8 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/routes/api/profile/update-password.zig create mode 100644 static/widgets/password-dialog/index.css create mode 100644 static/widgets/password-dialog/index.js diff --git a/src/routes/api/profile/root.zig b/src/routes/api/profile/root.zig index 04bf042..0bdd064 100644 --- a/src/routes/api/profile/root.zig +++ b/src/routes/api/profile/root.zig @@ -3,3 +3,4 @@ const HandlerInfo = memora.routes.HandlerInfo; pub const image = @import("image/root.zig"); pub const set: HandlerInfo = .from_type(@import("set.zig")); +pub const @"update-password": HandlerInfo = .from_type(@import("update-password.zig")); diff --git a/src/routes/api/profile/update-password.zig b/src/routes/api/profile/update-password.zig new file mode 100644 index 0000000..ac6ceed --- /dev/null +++ b/src/routes/api/profile/update-password.zig @@ -0,0 +1,37 @@ +const std = @import("std"); + +const memora = @import("memora"); +const Context = memora.Context; +const Storage = memora.Storage; + +pub const access = .users; + +const Body = struct { + current_password: []const u8, + new_password: []const u8, +}; + +const Response = struct { + success: bool, +}; + +pub fn post(ctx: *Context, body: Body) !Response { + if (ctx.storage.sessions.get(ctx.storage, ctx.fingerprint)) |session| { + var user = try Storage.User.open(ctx.storage, session.info.name); + defer user.deinit(); + + const result = Response { + .success = user.check_password(body.current_password) + }; + + if (result.success) { + try user.set_password(body.new_password); + try user.sync(); + } + + return result; + + } else { + return error.UnknownSession; + } +} diff --git a/src/storage/user.zig b/src/storage/user.zig index 86d82ff..b817f84 100644 --- a/src/storage/user.zig +++ b/src/storage/user.zig @@ -125,7 +125,7 @@ pub fn new( } pub fn set_password(self: *Self, password: []const u8) !void { - const allocator = self.area.allocator(); + const allocator = self.arena.allocator(); const hash_buf = try allocator.alloc(u8, 256); const hash = try std.crypto.pwhash.bcrypt.strHash( password, diff --git a/static/api/profile.js b/static/api/profile.js index ec633e0..d6acd16 100644 --- a/static/api/profile.js +++ b/static/api/profile.js @@ -6,3 +6,10 @@ export async function set(name, birthday) { birthday: birthday, }); } + +export function update_password(old, next) { + return rest.post('/api/profile/update-password', { + current_password: old, + new_password: next, + }).then(r => r.success); +} diff --git a/static/pages/settings/index.css b/static/pages/settings/index.css index 0d6b234..dcd952f 100644 --- a/static/pages/settings/index.css +++ b/static/pages/settings/index.css @@ -78,6 +78,18 @@ font-family: 'Pacifico'; } +#change-password { + background: var(--primary); + padding: 10px; + border-radius: var(--border-radius); + color: var(--fg-primary); + box-shadow: var(--shadow); + font-weight: bold; + text-align: center; + cursor: pointer; + user-select: none; +} + #logout { padding: 10px; width: 100%; @@ -88,4 +100,5 @@ font-weight: bold; border-radius: var(--border-radius); cursor: pointer; + user-select: none; } diff --git a/static/pages/settings/index.js b/static/pages/settings/index.js index 36c1d09..4924f8b 100644 --- a/static/pages/settings/index.js +++ b/static/pages/settings/index.js @@ -4,6 +4,7 @@ const { Div, Img } = sfw.element.native; import * as api from '../../api/index.js'; import Editable from '../../widgets/editable/index.js'; +import PasswordDialog from '../../widgets/password-dialog/index.js'; import icons from '../../icons/index.js'; @@ -15,6 +16,7 @@ export default class SettingsView extends sfw.element.Container { #birthday #profile + #change_password constructor() { super({ css }); @@ -63,6 +65,14 @@ export default class SettingsView extends sfw.element.Container { value: '', onupdate: () => this.#update(), }), + Div.new({ + id: 'change-password', + innerText: 'Change Password', + onclick: (e) => { + e.stopPropagation(); + this.body.append(PasswordDialog.new()); + } + }), Div.new({ id: 'logout', innerText: 'Logout', diff --git a/static/widgets/password-dialog/index.css b/static/widgets/password-dialog/index.css new file mode 100644 index 0000000..ad8c1c2 --- /dev/null +++ b/static/widgets/password-dialog/index.css @@ -0,0 +1,51 @@ +#container { + position: absolute; + max-width: 500px; + width: 70%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: grid; + gap: 10px; + background: #efefef99; + backdrop-filter: blur(10px); + padding: 10px; + border-radius: var(--border-radius); + box-shadow: var(--shadow); +} + +#title { + font-size: 1.1em; +} + +#close { + width: 25px; + height: 25px; + position: absolute; + top: 5px; + right: 5px; + cursor: pointer; + background: var(--page-background); + border-radius: 100%; + padding: 2px; +} + +#button { + background: var(--primary); + padding: 10px; + color: var(--fg-primary); + font-weight: bold; + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); + text-align: center; +} + +#error { + font-style: italic; + font-weight: bold; + color: #ee5151; +} + +#error.hidden { + display: none; +} diff --git a/static/widgets/password-dialog/index.js b/static/widgets/password-dialog/index.js new file mode 100644 index 0000000..334e02d --- /dev/null +++ b/static/widgets/password-dialog/index.js @@ -0,0 +1,60 @@ +import * as sfw from 'sfw'; +const { Div, Input } = sfw.element.native; + +import * as api from '../../api/index.js'; + +import icons from '../../icons/index.js'; + +const css = await sfw.css(import.meta.url, './index.css'); + +export default class PasswordDialog extends sfw.element.Container { + #current + #new + #confirm + #error + + constructor() { + super({ css }); + + this.callclose = () => this.close(); + + document.body.addEventListener('click', this.callclose); + this.onclick = (e) => e.stopPropagation(); + + this.body.append(Div.new({ + id: 'container', + children: [ + Div.new({ id: 'title', innerText: 'Change Password' }), + Div.new({ id: 'close', children: [ icons.close ], onclick: () => this.close() }), + this.#current = Input.new({ placeholder: 'Current Password', type: 'password' }), + this.#new = Input.new({ placeholder: 'New Password', type: 'password' }), + this.#confirm = Input.new({ placeholder: 'Confirm Password', type: 'password' }), + this.#error = Div.new({ id: 'error', className: 'hidden' }), + Div.new({ + id: 'button', + innerText: 'Update', + onclick: async () => { + if (this.#new.value !== this.#confirm.value) { + this.#error.innerText = 'Passwords do not match'; + this.#error.className = ''; + return; + } + + if (!await api.profile.update_password(this.#current.value, this.#new.value)) { + this.#error.innerText = 'invalid password'; + this.#error.className = ''; + return; + } + + this.close(); + }, + }), + ], + })); + } + + close() { + this.parentNode.removeChild(this); + document.body.removeEventListener('click', this.callclose); + } +} -- cgit v1.2.3-70-g09d2