feat(auth): add username field to user schema and authentication process
All checks were successful
Build Docker Image / build (push) Successful in 1m12s
All checks were successful
Build Docker Image / build (push) Successful in 1m12s
- Updated user schema to include a unique username field. - Modified authentication logic to support sign-in using either email or username. - Enhanced sign-up process to require a username and validate its uniqueness. - Updated login and profile routes to reflect changes in user identification. - Adjusted frontend forms to accommodate username input alongside email.
This commit is contained in:
51
drizzle/0009_true_gravity.sql
Normal file
51
drizzle/0009_true_gravity.sql
Normal file
@@ -0,0 +1,51 @@
|
||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
ALTER TABLE `user` ADD `username` text;--> statement-breakpoint
|
||||
UPDATE `user`
|
||||
SET `username` = `name`
|
||||
WHERE `username` IS NULL OR trim(`username`) = '';--> statement-breakpoint
|
||||
UPDATE `user`
|
||||
SET `username` = `username` || '_' || substr(`id`, 1, 6)
|
||||
WHERE `id` IN (
|
||||
SELECT u1.`id`
|
||||
FROM `user` u1
|
||||
JOIN `user` u2
|
||||
ON lower(u1.`username`) = lower(u2.`username`)
|
||||
AND u1.`id` > u2.`id`
|
||||
);--> statement-breakpoint
|
||||
CREATE TABLE `__new_user` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`username` text NOT NULL,
|
||||
`email` text NOT NULL,
|
||||
`email_verified` integer DEFAULT false NOT NULL,
|
||||
`image` text,
|
||||
`is_admin` integer DEFAULT false NOT NULL,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
|
||||
);--> statement-breakpoint
|
||||
INSERT INTO `__new_user` (
|
||||
`id`,
|
||||
`name`,
|
||||
`username`,
|
||||
`email`,
|
||||
`email_verified`,
|
||||
`image`,
|
||||
`is_admin`,
|
||||
`created_at`,
|
||||
`updated_at`
|
||||
)
|
||||
SELECT
|
||||
`id`,
|
||||
`name`,
|
||||
`username`,
|
||||
`email`,
|
||||
`email_verified`,
|
||||
`image`,
|
||||
`is_admin`,
|
||||
`created_at`,
|
||||
`updated_at`
|
||||
FROM `user`;--> statement-breakpoint
|
||||
DROP TABLE `user`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_user` RENAME TO `user`;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `user_username_unique` ON `user` (`username`);--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;
|
||||
1276
drizzle/meta/0009_snapshot.json
Normal file
1276
drizzle/meta/0009_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -64,6 +64,13 @@
|
||||
"when": 1772821532270,
|
||||
"tag": "0008_skinny_warpath",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "6",
|
||||
"when": 1772822823122,
|
||||
"tag": "0009_true_gravity",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,6 +9,15 @@ export const auth = betterAuth({
|
||||
baseURL: env.ORIGIN,
|
||||
secret: env.BETTER_AUTH_SECRET || 'secret',
|
||||
database: drizzleAdapter(db, { provider: 'sqlite' }),
|
||||
user: {
|
||||
additionalFields: {
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
unique: true
|
||||
}
|
||||
}
|
||||
},
|
||||
emailAndPassword: { enabled: true },
|
||||
plugins: [sveltekitCookies(getRequestEvent)] // make sure this is the last plugin in the array
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
||||
export const user = sqliteTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
username: text("username").notNull().unique(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: integer("email_verified", { mode: "boolean" })
|
||||
.default(false)
|
||||
|
||||
@@ -2,7 +2,10 @@ import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions } from './$types';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { auth } from '$lib/server/auth';
|
||||
import { db } from '$lib/server/db';
|
||||
import { user } from '$lib/server/db/schema';
|
||||
import { APIError } from 'better-auth/api';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
if (event.locals.user) {
|
||||
@@ -14,9 +17,28 @@ export const load: PageServerLoad = async (event) => {
|
||||
export const actions: Actions = {
|
||||
signInEmail: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
const email = formData.get('email')?.toString() ?? '';
|
||||
const identifier = formData.get('identifier')?.toString().trim() ?? formData.get('email')?.toString().trim() ?? '';
|
||||
const password = formData.get('password')?.toString() ?? '';
|
||||
|
||||
if (!identifier) {
|
||||
return fail(400, { message: 'Email ou nom d\'utilisateur requis' });
|
||||
}
|
||||
|
||||
let email = identifier;
|
||||
if (!identifier.includes('@')) {
|
||||
const [foundUser] = await db
|
||||
.select({ email: user.email })
|
||||
.from(user)
|
||||
.where(sql`lower(${user.username}) = ${identifier.toLowerCase()}`)
|
||||
.limit(1);
|
||||
|
||||
if (!foundUser) {
|
||||
return fail(400, { message: 'Identifiants invalides' });
|
||||
}
|
||||
|
||||
email = foundUser.email;
|
||||
}
|
||||
|
||||
try {
|
||||
await auth.api.signInEmail({
|
||||
body: {
|
||||
@@ -38,7 +60,33 @@ export const actions: Actions = {
|
||||
const formData = await event.request.formData();
|
||||
const email = formData.get('email')?.toString() ?? '';
|
||||
const password = formData.get('password')?.toString() ?? '';
|
||||
const confirmPassword = formData.get('confirmPassword')?.toString() ?? '';
|
||||
const name = formData.get('name')?.toString() ?? '';
|
||||
const username = formData.get('username')?.toString().trim() ?? '';
|
||||
|
||||
if (!username) {
|
||||
return fail(400, { message: 'Nom d\'utilisateur requis' });
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9._-]{3,30}$/.test(username)) {
|
||||
return fail(400, {
|
||||
message: "Le nom d'utilisateur doit contenir 3 à 30 caractères (lettres, chiffres, ., _, -)"
|
||||
});
|
||||
}
|
||||
|
||||
const [existingUsername] = await db
|
||||
.select({ id: user.id })
|
||||
.from(user)
|
||||
.where(sql`lower(${user.username}) = ${username.toLowerCase()}`)
|
||||
.limit(1);
|
||||
|
||||
if (existingUsername) {
|
||||
return fail(400, { message: "Ce nom d'utilisateur est déjà pris" });
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
return fail(400, { message: 'Les mots de passe ne correspondent pas' });
|
||||
}
|
||||
|
||||
try {
|
||||
await auth.api.signUpEmail({
|
||||
@@ -46,6 +94,7 @@ export const actions: Actions = {
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
username,
|
||||
callbackURL: '/auth/verification-success'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
let isSignUp = false;
|
||||
let name = '';
|
||||
let username = '';
|
||||
let email = '';
|
||||
let password = '';
|
||||
let confirmPassword = '';
|
||||
@@ -14,6 +15,7 @@
|
||||
const handleToggle = () => {
|
||||
isSignUp = !isSignUp;
|
||||
name = '';
|
||||
username = '';
|
||||
email = '';
|
||||
password = '';
|
||||
confirmPassword = '';
|
||||
@@ -75,18 +77,36 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Email Field -->
|
||||
<!-- Username Field (Sign Up Only) -->
|
||||
{#if isSignUp}
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||
E-mail
|
||||
<label for="username" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||
Nom d'utilisateur
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
id="username"
|
||||
type="text"
|
||||
name="username"
|
||||
bind:value={username}
|
||||
required
|
||||
placeholder="ex: luffy_gear5"
|
||||
class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Email / Username Field -->
|
||||
<div>
|
||||
<label for="identifier" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||
{isSignUp ? 'E-mail' : 'E-mail ou nom d\'utilisateur'}
|
||||
</label>
|
||||
<input
|
||||
id="identifier"
|
||||
type={isSignUp ? 'email' : 'text'}
|
||||
name={isSignUp ? 'email' : 'identifier'}
|
||||
bind:value={email}
|
||||
required
|
||||
placeholder="votremail@email.com"
|
||||
placeholder={isSignUp ? 'votremail@email.com' : 'votremail@email.com ou luffy_gear5'}
|
||||
class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Actions, PageServerLoad } from './$types';
|
||||
import { auth } from '$lib/server/auth';
|
||||
import { db } from '$lib/server/db';
|
||||
import { session, userCharacterHistory, characterHistory, character, friendship, user } from '$lib/server/db/schema';
|
||||
import { and, desc, eq, or } from 'drizzle-orm';
|
||||
import { and, desc, eq, or, sql } from 'drizzle-orm';
|
||||
import { APIError } from 'better-auth/api';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
@@ -42,7 +42,7 @@ export const load: PageServerLoad = async (event) => {
|
||||
createdAt: friendship.createdAt,
|
||||
requesterId: friendship.requesterId,
|
||||
requesterName: user.name,
|
||||
requesterEmail: user.email,
|
||||
requesterUsername: user.username,
|
||||
requesterImage: user.image
|
||||
})
|
||||
.from(friendship)
|
||||
@@ -56,7 +56,7 @@ export const load: PageServerLoad = async (event) => {
|
||||
createdAt: friendship.createdAt,
|
||||
addresseeId: friendship.addresseeId,
|
||||
addresseeName: user.name,
|
||||
addresseeEmail: user.email,
|
||||
addresseeUsername: user.username,
|
||||
addresseeImage: user.image
|
||||
})
|
||||
.from(friendship)
|
||||
@@ -70,7 +70,7 @@ export const load: PageServerLoad = async (event) => {
|
||||
createdAt: friendship.createdAt,
|
||||
friendId: friendship.addresseeId,
|
||||
friendName: user.name,
|
||||
friendEmail: user.email,
|
||||
friendUsername: user.username,
|
||||
friendImage: user.image
|
||||
})
|
||||
.from(friendship)
|
||||
@@ -83,7 +83,7 @@ export const load: PageServerLoad = async (event) => {
|
||||
createdAt: friendship.createdAt,
|
||||
friendId: friendship.requesterId,
|
||||
friendName: user.name,
|
||||
friendEmail: user.email,
|
||||
friendUsername: user.username,
|
||||
friendImage: user.image
|
||||
})
|
||||
.from(friendship)
|
||||
@@ -201,25 +201,27 @@ export const actions: Actions = {
|
||||
}
|
||||
|
||||
const formData = await event.request.formData();
|
||||
const friendEmail = formData.get('friendEmail')?.toString().trim().toLowerCase() ?? '';
|
||||
const friendUsername = formData.get('friendUsername')?.toString().trim() ?? '';
|
||||
|
||||
if (!friendEmail) {
|
||||
return fail(400, { message: 'Email requis pour envoyer une demande' });
|
||||
if (!friendUsername) {
|
||||
return fail(400, { message: 'Nom d\'utilisateur requis pour envoyer une demande' });
|
||||
}
|
||||
|
||||
const me = event.locals.user;
|
||||
if (friendEmail === me.email?.toLowerCase()) {
|
||||
const myUsername = (me as { username?: string }).username;
|
||||
|
||||
if (myUsername && friendUsername.toLowerCase() === myUsername.toLowerCase()) {
|
||||
return fail(400, { message: 'Tu ne peux pas t\'ajouter toi-même' });
|
||||
}
|
||||
|
||||
const [targetUser] = await db
|
||||
.select({ id: user.id, email: user.email })
|
||||
.select({ id: user.id, username: user.username })
|
||||
.from(user)
|
||||
.where(eq(user.email, friendEmail))
|
||||
.where(sql`lower(${user.username}) = ${friendUsername.toLowerCase()}`)
|
||||
.limit(1);
|
||||
|
||||
if (!targetUser) {
|
||||
return fail(404, { message: 'Aucun utilisateur trouvé avec cet email' });
|
||||
return fail(404, { message: 'Aucun utilisateur trouvé avec ce nom d\'utilisateur' });
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
let isLoading = $state(false);
|
||||
let activeTab = $state<'profile' | 'password' | 'sessions' | 'daily' | 'friends'>('profile');
|
||||
let name = $state('');
|
||||
let friendEmail = $state('');
|
||||
let friendUsername = $state('');
|
||||
let showSuccess = $state(false);
|
||||
let oldPassword = $state('');
|
||||
let newPassword = $state('');
|
||||
@@ -222,23 +222,23 @@
|
||||
isLoading = true;
|
||||
return async ({ update }) => {
|
||||
isLoading = false;
|
||||
friendEmail = '';
|
||||
friendUsername = '';
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
class="mb-8 space-y-3"
|
||||
>
|
||||
<label for="friendEmail" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||
Ajouter un ami par email
|
||||
<label for="friendUsername" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||
Ajouter un ami par nom d'utilisateur
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
id="friendEmail"
|
||||
type="email"
|
||||
name="friendEmail"
|
||||
id="friendUsername"
|
||||
type="text"
|
||||
name="friendUsername"
|
||||
required
|
||||
bind:value={friendEmail}
|
||||
placeholder="ami@email.com"
|
||||
bind:value={friendUsername}
|
||||
placeholder="ex: luffy_gear5"
|
||||
class="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
|
||||
/>
|
||||
<button
|
||||
@@ -265,7 +265,7 @@
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3">
|
||||
<div>
|
||||
<p class="font-semibold text-white">{req.requesterName}</p>
|
||||
<p class="text-xs text-slate-400">{req.requesterEmail}</p>
|
||||
<p class="text-xs text-slate-400">@{req.requesterUsername}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<form method="POST" action="?/acceptFriendRequest" use:enhance>
|
||||
@@ -293,7 +293,7 @@
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3">
|
||||
<div>
|
||||
<p class="font-semibold text-white">{req.addresseeName}</p>
|
||||
<p class="text-xs text-slate-400">{req.addresseeEmail}</p>
|
||||
<p class="text-xs text-slate-400">@{req.addresseeUsername}</p>
|
||||
</div>
|
||||
<form method="POST" action="?/cancelFriendRequest" use:enhance>
|
||||
<input type="hidden" name="friendshipId" value={req.id} />
|
||||
@@ -315,7 +315,7 @@
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3">
|
||||
<div>
|
||||
<p class="font-semibold text-white">{friend.friendName}</p>
|
||||
<p class="text-xs text-slate-400">{friend.friendEmail}</p>
|
||||
<p class="text-xs text-slate-400">@{friend.friendUsername}</p>
|
||||
</div>
|
||||
<form method="POST" action="?/removeFriend" use:enhance>
|
||||
<input type="hidden" name="friendshipId" value={friend.id} />
|
||||
|
||||
Reference in New Issue
Block a user