From 6b58af6d046c2f705b57a55059b6b069c488ea59 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sat, 16 May 2026 23:44:51 +0300 Subject: [PATCH] register: show specific errors and submit via fetch to preserve form Map UNIQUE constraint violations and other registration failures to descriptive messages instead of a generic "Failed to register", and submit the form via JS with no_redirect=1 so the page is not reloaded and the user's input is kept on error. --- static/js/register.js | 49 ++++++++++++++++++++++++++++++++ templates/register.html | 8 ++---- user/user.v | 57 ++++++++++++++++++++++++++++++++----- user/user_routes.v | 63 +++++++++++++++++++++++------------------ 4 files changed, 137 insertions(+), 40 deletions(-) create mode 100644 static/js/register.js diff --git a/static/js/register.js b/static/js/register.js new file mode 100644 index 0000000..141b88c --- /dev/null +++ b/static/js/register.js @@ -0,0 +1,49 @@ +const registerForm = document.getElementById("register-form"); +const registerError = document.getElementById("register-error"); +const registerSubmit = registerForm.querySelector("input[type=submit]"); + +function showRegisterError(msg) { + registerError.textContent = msg; + registerError.classList.add("alert"); + registerError.style.display = ""; +} + +function clearRegisterError() { + registerError.textContent = ""; + registerError.classList.remove("alert"); +} + +registerError.addEventListener("click", clearRegisterError); + +registerForm.addEventListener("submit", async (event) => { + event.preventDefault(); + + registerSubmit.disabled = true; + clearRegisterError(); + + const data = new FormData(registerForm); + data.set("no_redirect", "1"); + + let response; + try { + response = await fetch(registerForm.action, { + method: "POST", + body: data, + }); + } catch (err) { + showRegisterError("Network error: " + err.message); + registerSubmit.disabled = false; + return; + } + + const body = (await response.text()).trim(); + + if (response.ok && body === "ok") { + const username = data.get("username"); + window.location.href = "/" + username; + return; + } + + showRegisterError(body || "Failed to register"); + registerSubmit.disabled = false; +}); diff --git a/templates/register.html b/templates/register.html index 0c7da5b..1f2e099 100644 --- a/templates/register.html +++ b/templates/register.html @@ -14,9 +14,7 @@ @include 'layout/header.html'
- .form-error { - @ctx.form_error - } +
@ctx.form_error

@if no_users @@ -33,7 +31,7 @@ @end
-
+
@@ -50,7 +48,7 @@

- @js '/js/block-form.js' + @js '/js/register.js' @include 'layout/footer.html' diff --git a/user/user.v b/user/user.v index c2cfb93..c706880 100644 --- a/user/user.v +++ b/user/user.v @@ -67,12 +67,30 @@ pub fn (mut app App) register_user(username string, password string, salt string if user.id != 0 && user.is_registered { app.info('User ${username} already exists') - return false + return error('username `${username}` is already taken') + } + + // A non-registered row with this username exists (e.g. a GitHub shadow user). + // Block normal registration; the GitHub flow handles upgrading shadow users itself. + if user.id != 0 && !github { + app.info('Username ${username} is reserved by an unregistered/shadow user') + return error('username `${username}` is already taken') } user = app.get_user_by_email(emails[0]) or { User{} } + if user.id != 0 && user.is_registered { + app.info('Email ${emails[0]} is already in use') + return error('email `${emails[0]}` is already in use') + } + if user.id == 0 { + // Final guard: make sure no Email row points at this address even if + // the parent user lookup didn't surface (orphaned/duplicate rows). + if app.email_exists(emails[0]) { + return error('email `${emails[0]}` is already in use') + } + user = User{ username: username password: password @@ -85,22 +103,36 @@ pub fn (mut app App) register_user(username string, password string, salt string is_admin: is_admin } - app.add_user(user)! + app.add_user(user) or { + if is_unique_constraint_error(err) { + return error('username `${username}` or email `${emails[0]}` is already in use') + } + return err + } mut u := app.get_user_by_username(user.username) or { app.info('User was not inserted') - return false + return error('user `${username}` was not inserted (lookup after insert failed: ${err})') } - if u.password != user.password || u.username != user.username { - app.info('User was not inserted') - return false + if u.password != user.password { + app.info('User was not inserted (password mismatch after insert)') + return error('user `${username}` was not inserted (password mismatch after insert)') + } + if u.username != user.username { + app.info('User was not inserted (username mismatch after insert)') + return error('user `${username}` was not inserted (username mismatch after insert: got `${u.username}`)') } app.add_activity(u.id, 'joined')! for email in emails { - app.add_email(u.id, email)! + app.add_email(u.id, email) or { + if is_unique_constraint_error(err) { + return error('email `${email}` is already in use') + } + return err + } } u.emails = app.find_user_emails(u.id) @@ -124,6 +156,17 @@ pub fn (mut app App) register_user(username string, password string, salt string return true } +fn is_unique_constraint_error(err IError) bool { + return err.msg().to_lower().contains('unique constraint') +} + +pub fn (app App) email_exists(value string) bool { + rows := sql app.db { + select from Email where email == value limit 1 + } or { [] } + return rows.len > 0 +} + fn (mut app App) create_user_dir(username string) { user_path := '${app.config.repo_storage_path}/${username}' diff --git a/user/user_routes.v b/user/user_routes.v index 9acc6e9..841523a 100644 --- a/user/user_routes.v +++ b/user/user_routes.v @@ -182,80 +182,87 @@ pub fn (mut app App) register(mut ctx Context) veb.Result { return $veb.html() } +fn (mut app App) register_failed(mut ctx Context, no_redirect string, msg string) veb.Result { + if no_redirect == '1' { + ctx.res.set_status(.bad_request) + return ctx.text(msg) + } + ctx.error(msg) + return app.register(mut ctx) +} + @['/register'; post] pub fn (mut app App) handle_register(mut ctx Context, username string, email string, password string, no_redirect string) veb.Result { user_count := app.get_users_count() or { - ctx.error('Failed to register') - return app.register(mut ctx) + eprintln('[register] get_users_count failed: ${err}') + return app.register_failed(mut ctx, no_redirect, 'Failed to register: ${err}') } no_users := user_count == 0 println('USERNAME=${username}') if username in ['login', 'register', 'new', 'new_post', 'oauth'] { - ctx.error('Username `${username}` is not available') - return app.register(mut ctx) + return app.register_failed(mut ctx, no_redirect, 'Username `${username}` is not available') } user_chars := username.bytes() if user_chars.len > max_username_len { - ctx.error('Username is too long (max. ${max_username_len})') - return app.register(mut ctx) + return app.register_failed(mut ctx, no_redirect, + 'Username is too long (max. ${max_username_len})') } if username.contains('--') { - ctx.error('Username cannot contain two hyphens') - return app.register(mut ctx) + return app.register_failed(mut ctx, no_redirect, 'Username cannot contain two hyphens') } if user_chars[0] == `-` || user_chars.last() == `-` { - ctx.error('Username cannot begin or end with a hyphen') - return app.register(mut ctx) + return app.register_failed(mut ctx, no_redirect, + 'Username cannot begin or end with a hyphen') } for ch in user_chars { if !ch.is_letter() && !ch.is_digit() && ch != `-` { - ctx.error('Username cannot contain special characters') - return app.register(mut ctx) + return app.register_failed(mut ctx, no_redirect, + 'Username cannot contain special characters') } } is_username_valid := validation.is_username_valid(username) if !is_username_valid { - ctx.error('Username is not valid') - - return app.register(mut ctx) + return app.register_failed(mut ctx, no_redirect, 'Username is not valid') } if password == '' { - ctx.error('Password cannot be empty') - - return app.register(mut ctx) + return app.register_failed(mut ctx, no_redirect, 'Password cannot be empty') } salt := generate_salt() hashed_password := hash_password_with_salt(password, salt) if username == '' || email == '' { - ctx.error('Username or Email cannot be emtpy') - return app.register(mut ctx) + return app.register_failed(mut ctx, no_redirect, 'Username or Email cannot be emtpy') } // TODO: refactor is_registered := app.register_user(username, hashed_password, salt, [email], false, no_users) or { - ctx.error('Failed to register') - return app.register(mut ctx) + eprintln('[register] register_user failed for username=${username} email=${email}: ${err}') + msg := if is_unique_constraint_error(err) { + 'Username `${username}` or email `${email}` is already in use' + } else { + 'Failed to register: ${err.msg()}' + } + return app.register_failed(mut ctx, no_redirect, msg) } if !is_registered { - ctx.error('Failed to register') - return app.register(mut ctx) + eprintln('[register] register_user returned false for username=${username} email=${email} (user already exists or insertion mismatch — see prior info logs)') + return app.register_failed(mut ctx, no_redirect, + 'Failed to register: user already exists or could not be inserted') } user := app.get_user_by_username(username) or { - ctx.error('User already exists') - return app.register(mut ctx) + return app.register_failed(mut ctx, no_redirect, 'User already exists') } if no_users { @@ -265,8 +272,8 @@ pub fn (mut app App) handle_register(mut ctx Context, username string, email str client_ip := 'ip' // ctx.ip() // XTODO app.auth_user(mut ctx, user, client_ip) or { - ctx.error('Failed to register') - return app.register(mut ctx) + eprintln('[register] auth_user failed for username=${username}: ${err}') + return app.register_failed(mut ctx, no_redirect, 'Failed to register: ${err}') } app.add_security_log(user_id: user.id, kind: .registered) or { app.info(err.str()) } -- 2.39.5