aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2025-11-24 15:55:20 +0100
committerNathan Reiner <nathan@nathanreiner.xyz>2025-11-24 15:55:20 +0100
commit4b6e37397d3a9a80db0c20484b712175c7b9c9c7 (patch)
treea741d76d250b7e7a53f3ab6715106463db32215e
parenta72a9c6fa8aacbd9e945fdce64bdaf7895425e95 (diff)
add password-dialog
-rw-r--r--src/routes/api/profile/root.zig1
-rw-r--r--src/routes/api/profile/update-password.zig37
-rw-r--r--src/storage/user.zig2
-rw-r--r--static/api/profile.js7
-rw-r--r--static/pages/settings/index.css13
-rw-r--r--static/pages/settings/index.js10
-rw-r--r--static/widgets/password-dialog/index.css51
-rw-r--r--static/widgets/password-dialog/index.js60
8 files changed, 180 insertions, 1 deletions
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 });
@@ -64,6 +66,14 @@ export default class SettingsView extends sfw.element.Container {
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',
onclick: () => this.onlogout(),
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);
+ }
+}