refactor: allow more characters in usernames
This commit is contained in:
parent
66ed89d39e
commit
3920b6c81b
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 { authClient } from "~/client/lib/auth-client";
|
||||||
import { ResetPasswordDialog } from "../components/reset-password-dialog";
|
import { ResetPasswordDialog } from "../components/reset-password-dialog";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { normalizeUsername } from "~/lib/username";
|
||||||
|
|
||||||
const loginSchema = type({
|
const loginSchema = type({
|
||||||
username: "2<=string<=50",
|
username: "2<=string<=50",
|
||||||
|
|
@ -39,7 +40,7 @@ export function LoginPage() {
|
||||||
|
|
||||||
const onSubmit = async (values: LoginFormValues) => {
|
const onSubmit = async (values: LoginFormValues) => {
|
||||||
const { data, error } = await authClient.signIn.username({
|
const { data, error } = await authClient.signIn.username({
|
||||||
username: values.username.toLowerCase().trim(),
|
username: normalizeUsername(values.username),
|
||||||
password: values.password,
|
password: values.password,
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
onRequest: () => {
|
onRequest: () => {
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,12 @@ import { Button } from "~/client/components/ui/button";
|
||||||
import { authClient } from "~/client/lib/auth-client";
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { normalizeUsername } from "~/lib/username";
|
||||||
|
|
||||||
export const clientMiddleware = [authMiddleware];
|
export const clientMiddleware = [authMiddleware];
|
||||||
|
|
||||||
const onboardingSchema = type({
|
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()),
|
email: type("string.email").pipe((str) => str.trim().toLowerCase()),
|
||||||
password: "string>=8",
|
password: "string>=8",
|
||||||
confirmPassword: "string>=1",
|
confirmPassword: "string>=1",
|
||||||
|
|
@ -54,7 +55,7 @@ export function OnboardingPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await authClient.signUp.email({
|
const { data, error } = await authClient.signUp.email({
|
||||||
username: values.username.toLowerCase().trim(),
|
username: normalizeUsername(values.username),
|
||||||
password: values.password,
|
password: values.password,
|
||||||
email: values.email.toLowerCase().trim(),
|
email: values.email.toLowerCase().trim(),
|
||||||
name: values.username,
|
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 type { AuthMiddlewareContext } from "../auth";
|
||||||
import { UnauthorizedError } from "http-errors-enhanced";
|
import { UnauthorizedError } from "http-errors-enhanced";
|
||||||
import { cryptoUtils } from "~/server/utils/crypto";
|
import { cryptoUtils } from "~/server/utils/crypto";
|
||||||
|
import { normalizeUsername } from "~/lib/username";
|
||||||
|
|
||||||
export const convertLegacyUserOnFirstLogin = async (ctx: AuthMiddlewareContext) => {
|
export const convertLegacyUserOnFirstLogin = async (ctx: AuthMiddlewareContext) => {
|
||||||
const { path, body } = ctx;
|
const { path, body } = ctx;
|
||||||
|
|
@ -16,7 +17,7 @@ export const convertLegacyUserOnFirstLogin = async (ctx: AuthMiddlewareContext)
|
||||||
const legacyUser = await db.query.usersTable.findFirst({
|
const legacyUser = await db.query.usersTable.findFirst({
|
||||||
where: {
|
where: {
|
||||||
AND: [
|
AND: [
|
||||||
{ username: body.username.trim().toLowerCase() },
|
{ username: normalizeUsername(body.username) },
|
||||||
{ passwordHash: { NOT: "" } },
|
{ passwordHash: { NOT: "" } },
|
||||||
{ passwordHash: { isNotNull: true } },
|
{ 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 { ensureOnlyOneUser } from "./auth-middlewares/only-one-user";
|
||||||
import { authService } from "../modules/auth/auth.service";
|
import { authService } from "../modules/auth/auth.service";
|
||||||
import { tanstackStartCookies } from "better-auth/tanstack-start";
|
import { tanstackStartCookies } from "better-auth/tanstack-start";
|
||||||
|
import { isValidUsername, normalizeUsername } from "~/lib/username";
|
||||||
|
|
||||||
export type AuthMiddlewareContext = MiddlewareContext<MiddlewareOptions, AuthContext<BetterAuthOptions>>;
|
export type AuthMiddlewareContext = MiddlewareContext<MiddlewareOptions, AuthContext<BetterAuthOptions>>;
|
||||||
|
|
||||||
|
|
@ -140,7 +141,10 @@ export const auth = betterAuth({
|
||||||
modelName: "sessionsTable",
|
modelName: "sessionsTable",
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
username(),
|
username({
|
||||||
|
usernameValidator: isValidUsername,
|
||||||
|
usernameNormalization: normalizeUsername,
|
||||||
|
}),
|
||||||
admin({
|
admin({
|
||||||
defaultRole: "user",
|
defaultRole: "user",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue