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,
|
"when": 1772821532270,
|
||||||
"tag": "0008_skinny_warpath",
|
"tag": "0008_skinny_warpath",
|
||||||
"breakpoints": true
|
"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,
|
baseURL: env.ORIGIN,
|
||||||
secret: env.BETTER_AUTH_SECRET || 'secret',
|
secret: env.BETTER_AUTH_SECRET || 'secret',
|
||||||
database: drizzleAdapter(db, { provider: 'sqlite' }),
|
database: drizzleAdapter(db, { provider: 'sqlite' }),
|
||||||
|
user: {
|
||||||
|
additionalFields: {
|
||||||
|
username: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
unique: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
emailAndPassword: { enabled: true },
|
emailAndPassword: { enabled: true },
|
||||||
plugins: [sveltekitCookies(getRequestEvent)] // make sure this is the last plugin in the array
|
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", {
|
export const user = sqliteTable("user", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
|
username: text("username").notNull().unique(),
|
||||||
email: text("email").notNull().unique(),
|
email: text("email").notNull().unique(),
|
||||||
emailVerified: integer("email_verified", { mode: "boolean" })
|
emailVerified: integer("email_verified", { mode: "boolean" })
|
||||||
.default(false)
|
.default(false)
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { fail, redirect } from '@sveltejs/kit';
|
|||||||
import type { Actions } from './$types';
|
import type { Actions } from './$types';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { auth } from '$lib/server/auth';
|
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 { APIError } from 'better-auth/api';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
|
||||||
export const load: PageServerLoad = async (event) => {
|
export const load: PageServerLoad = async (event) => {
|
||||||
if (event.locals.user) {
|
if (event.locals.user) {
|
||||||
@@ -14,9 +17,28 @@ export const load: PageServerLoad = async (event) => {
|
|||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
signInEmail: async (event) => {
|
signInEmail: async (event) => {
|
||||||
const formData = await event.request.formData();
|
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() ?? '';
|
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 {
|
try {
|
||||||
await auth.api.signInEmail({
|
await auth.api.signInEmail({
|
||||||
body: {
|
body: {
|
||||||
@@ -38,7 +60,33 @@ export const actions: Actions = {
|
|||||||
const formData = await event.request.formData();
|
const formData = await event.request.formData();
|
||||||
const email = formData.get('email')?.toString() ?? '';
|
const email = formData.get('email')?.toString() ?? '';
|
||||||
const password = formData.get('password')?.toString() ?? '';
|
const password = formData.get('password')?.toString() ?? '';
|
||||||
|
const confirmPassword = formData.get('confirmPassword')?.toString() ?? '';
|
||||||
const name = formData.get('name')?.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 {
|
try {
|
||||||
await auth.api.signUpEmail({
|
await auth.api.signUpEmail({
|
||||||
@@ -46,6 +94,7 @@ export const actions: Actions = {
|
|||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
name,
|
name,
|
||||||
|
username,
|
||||||
callbackURL: '/auth/verification-success'
|
callbackURL: '/auth/verification-success'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
let isSignUp = false;
|
let isSignUp = false;
|
||||||
let name = '';
|
let name = '';
|
||||||
|
let username = '';
|
||||||
let email = '';
|
let email = '';
|
||||||
let password = '';
|
let password = '';
|
||||||
let confirmPassword = '';
|
let confirmPassword = '';
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
isSignUp = !isSignUp;
|
isSignUp = !isSignUp;
|
||||||
name = '';
|
name = '';
|
||||||
|
username = '';
|
||||||
email = '';
|
email = '';
|
||||||
password = '';
|
password = '';
|
||||||
confirmPassword = '';
|
confirmPassword = '';
|
||||||
@@ -75,18 +77,36 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Email Field -->
|
<!-- Username Field (Sign Up Only) -->
|
||||||
|
{#if isSignUp}
|
||||||
|
<div>
|
||||||
|
<label for="username" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||||
|
Nom d'utilisateur
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
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>
|
<div>
|
||||||
<label for="email" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
<label for="identifier" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||||
E-mail
|
{isSignUp ? 'E-mail' : 'E-mail ou nom d\'utilisateur'}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="email"
|
id="identifier"
|
||||||
type="email"
|
type={isSignUp ? 'email' : 'text'}
|
||||||
name="email"
|
name={isSignUp ? 'email' : 'identifier'}
|
||||||
bind:value={email}
|
bind:value={email}
|
||||||
required
|
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"
|
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>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Actions, PageServerLoad } from './$types';
|
|||||||
import { auth } from '$lib/server/auth';
|
import { auth } from '$lib/server/auth';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { session, userCharacterHistory, characterHistory, character, friendship, user } from '$lib/server/db/schema';
|
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';
|
import { APIError } from 'better-auth/api';
|
||||||
|
|
||||||
export const load: PageServerLoad = async (event) => {
|
export const load: PageServerLoad = async (event) => {
|
||||||
@@ -42,7 +42,7 @@ export const load: PageServerLoad = async (event) => {
|
|||||||
createdAt: friendship.createdAt,
|
createdAt: friendship.createdAt,
|
||||||
requesterId: friendship.requesterId,
|
requesterId: friendship.requesterId,
|
||||||
requesterName: user.name,
|
requesterName: user.name,
|
||||||
requesterEmail: user.email,
|
requesterUsername: user.username,
|
||||||
requesterImage: user.image
|
requesterImage: user.image
|
||||||
})
|
})
|
||||||
.from(friendship)
|
.from(friendship)
|
||||||
@@ -56,7 +56,7 @@ export const load: PageServerLoad = async (event) => {
|
|||||||
createdAt: friendship.createdAt,
|
createdAt: friendship.createdAt,
|
||||||
addresseeId: friendship.addresseeId,
|
addresseeId: friendship.addresseeId,
|
||||||
addresseeName: user.name,
|
addresseeName: user.name,
|
||||||
addresseeEmail: user.email,
|
addresseeUsername: user.username,
|
||||||
addresseeImage: user.image
|
addresseeImage: user.image
|
||||||
})
|
})
|
||||||
.from(friendship)
|
.from(friendship)
|
||||||
@@ -70,7 +70,7 @@ export const load: PageServerLoad = async (event) => {
|
|||||||
createdAt: friendship.createdAt,
|
createdAt: friendship.createdAt,
|
||||||
friendId: friendship.addresseeId,
|
friendId: friendship.addresseeId,
|
||||||
friendName: user.name,
|
friendName: user.name,
|
||||||
friendEmail: user.email,
|
friendUsername: user.username,
|
||||||
friendImage: user.image
|
friendImage: user.image
|
||||||
})
|
})
|
||||||
.from(friendship)
|
.from(friendship)
|
||||||
@@ -83,7 +83,7 @@ export const load: PageServerLoad = async (event) => {
|
|||||||
createdAt: friendship.createdAt,
|
createdAt: friendship.createdAt,
|
||||||
friendId: friendship.requesterId,
|
friendId: friendship.requesterId,
|
||||||
friendName: user.name,
|
friendName: user.name,
|
||||||
friendEmail: user.email,
|
friendUsername: user.username,
|
||||||
friendImage: user.image
|
friendImage: user.image
|
||||||
})
|
})
|
||||||
.from(friendship)
|
.from(friendship)
|
||||||
@@ -201,25 +201,27 @@ export const actions: Actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formData = await event.request.formData();
|
const formData = await event.request.formData();
|
||||||
const friendEmail = formData.get('friendEmail')?.toString().trim().toLowerCase() ?? '';
|
const friendUsername = formData.get('friendUsername')?.toString().trim() ?? '';
|
||||||
|
|
||||||
if (!friendEmail) {
|
if (!friendUsername) {
|
||||||
return fail(400, { message: 'Email requis pour envoyer une demande' });
|
return fail(400, { message: 'Nom d\'utilisateur requis pour envoyer une demande' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const me = event.locals.user;
|
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' });
|
return fail(400, { message: 'Tu ne peux pas t\'ajouter toi-même' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const [targetUser] = await db
|
const [targetUser] = await db
|
||||||
.select({ id: user.id, email: user.email })
|
.select({ id: user.id, username: user.username })
|
||||||
.from(user)
|
.from(user)
|
||||||
.where(eq(user.email, friendEmail))
|
.where(sql`lower(${user.username}) = ${friendUsername.toLowerCase()}`)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!targetUser) {
|
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
|
const [existing] = await db
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
let activeTab = $state<'profile' | 'password' | 'sessions' | 'daily' | 'friends'>('profile');
|
let activeTab = $state<'profile' | 'password' | 'sessions' | 'daily' | 'friends'>('profile');
|
||||||
let name = $state('');
|
let name = $state('');
|
||||||
let friendEmail = $state('');
|
let friendUsername = $state('');
|
||||||
let showSuccess = $state(false);
|
let showSuccess = $state(false);
|
||||||
let oldPassword = $state('');
|
let oldPassword = $state('');
|
||||||
let newPassword = $state('');
|
let newPassword = $state('');
|
||||||
@@ -222,23 +222,23 @@
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
return async ({ update }) => {
|
return async ({ update }) => {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
friendEmail = '';
|
friendUsername = '';
|
||||||
await update();
|
await update();
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
class="mb-8 space-y-3"
|
class="mb-8 space-y-3"
|
||||||
>
|
>
|
||||||
<label for="friendEmail" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
<label for="friendUsername" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||||
Ajouter un ami par email
|
Ajouter un ami par nom d'utilisateur
|
||||||
</label>
|
</label>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input
|
<input
|
||||||
id="friendEmail"
|
id="friendUsername"
|
||||||
type="email"
|
type="text"
|
||||||
name="friendEmail"
|
name="friendUsername"
|
||||||
required
|
required
|
||||||
bind:value={friendEmail}
|
bind:value={friendUsername}
|
||||||
placeholder="ami@email.com"
|
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"
|
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
|
<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 class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold text-white">{req.requesterName}</p>
|
<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>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<form method="POST" action="?/acceptFriendRequest" use:enhance>
|
<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 class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold text-white">{req.addresseeName}</p>
|
<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>
|
</div>
|
||||||
<form method="POST" action="?/cancelFriendRequest" use:enhance>
|
<form method="POST" action="?/cancelFriendRequest" use:enhance>
|
||||||
<input type="hidden" name="friendshipId" value={req.id} />
|
<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 class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold text-white">{friend.friendName}</p>
|
<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>
|
</div>
|
||||||
<form method="POST" action="?/removeFriend" use:enhance>
|
<form method="POST" action="?/removeFriend" use:enhance>
|
||||||
<input type="hidden" name="friendshipId" value={friend.id} />
|
<input type="hidden" name="friendshipId" value={friend.id} />
|
||||||
|
|||||||
Reference in New Issue
Block a user