refactor: allow more characters in usernames (#529)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
This commit is contained in:
parent
99bb296866
commit
dda7b9939f
6 changed files with 38 additions and 5 deletions
|
|
@ -12,6 +12,7 @@ import { Label } from "~/client/components/ui/label";
|
|||
import { authClient } from "~/client/lib/auth-client";
|
||||
import { ResetPasswordDialog } from "../components/reset-password-dialog";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { normalizeUsername } from "~/lib/username";
|
||||
|
||||
const loginSchema = type({
|
||||
username: "2<=string<=50",
|
||||
|
|
@ -39,7 +40,7 @@ export function LoginPage() {
|
|||
|
||||
const onSubmit = async (values: LoginFormValues) => {
|
||||
const { data, error } = await authClient.signIn.username({
|
||||
username: values.username.toLowerCase().trim(),
|
||||
username: normalizeUsername(values.username),
|
||||
password: values.password,
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
|
|
|
|||
|
|
@ -18,11 +18,12 @@ import { Button } from "~/client/components/ui/button";
|
|||
import { authClient } from "~/client/lib/auth-client";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { normalizeUsername } from "~/lib/username";
|
||||
|
||||
export const clientMiddleware = [authMiddleware];
|
||||
|
||||
const onboardingSchema = type({
|
||||
username: type("2<=string<=30").pipe((str) => str.trim().toLowerCase()),
|
||||
username: type("2<=string<=30").pipe(normalizeUsername),
|
||||
email: type("string.email").pipe((str) => str.trim().toLowerCase()),
|
||||
password: "string>=8",
|
||||
confirmPassword: "string>=1",
|
||||
|
|
@ -54,7 +55,7 @@ export function OnboardingPage() {
|
|||
}
|
||||
|
||||
const { data, error } = await authClient.signUp.email({
|
||||
username: values.username.toLowerCase().trim(),
|
||||
username: normalizeUsername(values.username),
|
||||
password: values.password,
|
||||
email: values.email.toLowerCase().trim(),
|
||||
name: values.username,
|
||||
|
|
|
|||
5
app/lib/username.ts
Normal file
5
app/lib/username.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const USERNAME_ALLOWED_CHARACTERS_REGEX = /^[a-z0-9_.-]+$/i;
|
||||
|
||||
export const normalizeUsername = (username: string): string => username.trim().toLowerCase();
|
||||
|
||||
export const isValidUsername = (username: string): boolean => USERNAME_ALLOWED_CHARACTERS_REGEX.test(username);
|
||||
21
app/server/lib/__tests__/username.test.ts
Normal file
21
app/server/lib/__tests__/username.test.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { describe, expect, test } from "bun:test";
|
||||
import { isValidUsername, normalizeUsername } from "~/lib/username";
|
||||
|
||||
describe("username helpers", () => {
|
||||
test("normalizes usernames by trimming and lowercasing", () => {
|
||||
expect(normalizeUsername(" Admin-User ")).toBe("admin-user");
|
||||
});
|
||||
|
||||
test("accepts usernames containing a hyphen", () => {
|
||||
expect(isValidUsername(normalizeUsername("Admin-User"))).toBe(true);
|
||||
});
|
||||
|
||||
test("accepts letters, numbers, dots, and underscores", () => {
|
||||
expect(isValidUsername("admin.user_01")).toBe(true);
|
||||
});
|
||||
|
||||
test("rejects usernames with unsupported characters", () => {
|
||||
expect(isValidUsername("admin user")).toBe(false);
|
||||
expect(isValidUsername("admin@user")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -5,6 +5,7 @@ import { account, usersTable, member, organization } from "~/server/db/schema";
|
|||
import type { AuthMiddlewareContext } from "../auth";
|
||||
import { UnauthorizedError } from "http-errors-enhanced";
|
||||
import { cryptoUtils } from "~/server/utils/crypto";
|
||||
import { normalizeUsername } from "~/lib/username";
|
||||
|
||||
export const convertLegacyUserOnFirstLogin = async (ctx: AuthMiddlewareContext) => {
|
||||
const { path, body } = ctx;
|
||||
|
|
@ -16,7 +17,7 @@ export const convertLegacyUserOnFirstLogin = async (ctx: AuthMiddlewareContext)
|
|||
const legacyUser = await db.query.usersTable.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{ username: body.username.trim().toLowerCase() },
|
||||
{ username: normalizeUsername(body.username) },
|
||||
{ passwordHash: { NOT: "" } },
|
||||
{ passwordHash: { isNotNull: true } },
|
||||
],
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { organization as organizationTable, member, usersTable } from "../db/sch
|
|||
import { ensureOnlyOneUser } from "./auth-middlewares/only-one-user";
|
||||
import { authService } from "../modules/auth/auth.service";
|
||||
import { tanstackStartCookies } from "better-auth/tanstack-start";
|
||||
import { isValidUsername, normalizeUsername } from "~/lib/username";
|
||||
|
||||
export type AuthMiddlewareContext = MiddlewareContext<MiddlewareOptions, AuthContext<BetterAuthOptions>>;
|
||||
|
||||
|
|
@ -140,7 +141,10 @@ export const auth = betterAuth({
|
|||
modelName: "sessionsTable",
|
||||
},
|
||||
plugins: [
|
||||
username(),
|
||||
username({
|
||||
usernameValidator: isValidUsername,
|
||||
usernameNormalization: normalizeUsername,
|
||||
}),
|
||||
admin({
|
||||
defaultRole: "user",
|
||||
}),
|
||||
|
|
|
|||
Loading…
Reference in a new issue