feat: enhance profile page and authentication with user avatar support

- Updated `next.config.ts` to allow images from various external sources, including LinkedIn and GitHub.
- Refactored `ProfilePage` to improve layout and display user avatar, name, and role more prominently.
- Enhanced `AuthButton` to show user avatar if available, improving user experience.
- Updated authentication logic in `auth.ts` to include user avatar and role in session management.
- Extended JWT type definitions to support new user fields (firstName, lastName, avatar, role) for better user data handling.
This commit is contained in:
Julien Froidefond
2025-09-30 23:15:21 +02:00
parent 307b3a8a14
commit d8ca4ef00b
5 changed files with 272 additions and 121 deletions

View File

@@ -2,6 +2,46 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: 'standalone',
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'media.licdn.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'avatars.githubusercontent.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'lh3.googleusercontent.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'cdn.discordapp.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'images.unsplash.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'via.placeholder.com',
port: '',
pathname: '/**',
},
],
},
turbopack: {
rules: {
'*.sql': ['raw'],

View File

@@ -140,64 +140,118 @@ export default function ProfilePage() {
<Header title="TowerControl" subtitle="Profil utilisateur" />
<div className="container mx-auto px-4 py-8">
<div className="max-w-2xl mx-auto">
<div className="max-w-4xl mx-auto">
{/* Header avec avatar et infos principales */}
<div className="bg-gradient-to-r from-[var(--primary)]/10 to-[var(--accent)]/10 rounded-xl p-8 mb-8 border border-[var(--primary)]/20">
<div className="flex flex-col md:flex-row items-center md:items-start gap-6">
{/* Avatar section */}
<div className="flex-shrink-0">
{profile.avatar ? (
<div className="relative">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={profile.avatar}
alt="Avatar"
className="w-24 h-24 rounded-full object-cover border-4 border-[var(--primary)]/30 shadow-lg"
/>
<div className="absolute -bottom-2 -right-2 w-8 h-8 bg-[var(--success)] rounded-full border-4 border-[var(--card)] flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
</div>
) : (
<div className="w-24 h-24 rounded-full bg-[var(--primary)]/20 border-4 border-[var(--primary)]/30 flex items-center justify-center">
<svg className="w-12 h-12 text-[var(--primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
)}
</div>
{/* User info */}
<div className="flex-1 text-center md:text-left">
<h1 className="text-3xl font-mono font-bold text-[var(--foreground)] mb-2">
{profile.name || `${profile.firstName || ''} ${profile.lastName || ''}`.trim() || profile.email}
</h1>
<p className="text-[var(--muted-foreground)] text-lg mb-4">{profile.email}</p>
<div className="flex flex-wrap gap-4 justify-center md:justify-start">
<div className="flex items-center gap-2 px-3 py-1 bg-[var(--card)] rounded-full border border-[var(--border)]">
<div className="w-2 h-2 bg-[var(--success)] rounded-full"></div>
<span className="text-sm font-medium text-[var(--foreground)]">{profile.role}</span>
</div>
<div className="flex items-center gap-2 px-3 py-1 bg-[var(--card)] rounded-full border border-[var(--border)]">
<svg className="w-4 h-4 text-[var(--muted-foreground)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span className="text-sm text-[var(--muted-foreground)]">
Membre depuis {new Date(profile.createdAt).toLocaleDateString('fr-FR')}
</span>
</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Informations générales */}
<div className="bg-[var(--card)] rounded-lg p-6 mb-6">
<h2 className="text-xl font-mono font-bold text-[var(--foreground)] mb-4">
<div className="bg-[var(--card)] rounded-xl p-6 border border-[var(--border)]">
<h2 className="text-xl font-mono font-bold text-[var(--foreground)] mb-6 flex items-center gap-2">
<svg className="w-5 h-5 text-[var(--primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Informations générales
</h2>
<div className="space-y-4">
<div className="flex items-center gap-3 p-3 bg-[var(--input)] rounded-lg border border-[var(--border)]">
<svg className="w-5 h-5 text-[var(--muted-foreground)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
</svg>
<div>
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
Email
</label>
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
{profile.email}
</div>
<p className="text-xs text-[var(--muted-foreground)] mt-1">
L&apos;email ne peut pas être modifié
</p>
</div>
<div>
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
Rôle
</label>
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
{profile.role}
<div className="text-[var(--foreground)] font-medium">{profile.email}</div>
<div className="text-xs text-[var(--muted-foreground)]">Email principal</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-[var(--input)] rounded-lg border border-[var(--border)]">
<svg className="w-5 h-5 text-[var(--muted-foreground)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
Membre depuis
</label>
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
{new Date(profile.createdAt).toLocaleDateString('fr-FR')}
<div className="text-[var(--foreground)] font-medium">{profile.role}</div>
<div className="text-xs text-[var(--muted-foreground)]">Rôle utilisateur</div>
</div>
</div>
{profile.lastLoginAt && (
<div className="flex items-center gap-3 p-3 bg-[var(--input)] rounded-lg border border-[var(--border)]">
<svg className="w-5 h-5 text-[var(--muted-foreground)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
Dernière connexion
</label>
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
<div className="text-[var(--foreground)] font-medium">
{new Date(profile.lastLoginAt).toLocaleString('fr-FR')}
</div>
<div className="text-xs text-[var(--muted-foreground)]">Dernière connexion</div>
</div>
</div>
)}
</div>
</div>
{/* Formulaire de modification */}
<form onSubmit={handleSubmit} className="bg-[var(--card)] rounded-lg p-6">
<h2 className="text-xl font-mono font-bold text-[var(--foreground)] mb-4">
<div className="bg-[var(--card)] rounded-xl p-6 border border-[var(--border)]">
<h2 className="text-xl font-mono font-bold text-[var(--foreground)] mb-6 flex items-center gap-2">
<svg className="w-5 h-5 text-[var(--primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Modifier le profil
</h2>
<div className="space-y-4">
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="firstName" className="block text-sm font-medium text-[var(--foreground)] mb-2">
Prénom
@@ -221,6 +275,7 @@ export default function ProfilePage() {
placeholder="Votre nom de famille"
/>
</div>
</div>
<div>
<label htmlFor="name" className="block text-sm font-medium text-[var(--foreground)] mb-2">
@@ -247,33 +302,68 @@ export default function ProfilePage() {
onChange={(e) => handleChange('avatar', e.target.value)}
placeholder="https://example.com/avatar.jpg"
/>
{formData.avatar && (
<div className="mt-2 flex items-center gap-2">
<span className="text-xs text-[var(--muted-foreground)]">Aperçu:</span>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={formData.avatar}
alt="Aperçu avatar"
className="w-8 h-8 rounded-full object-cover border border-[var(--border)]"
onError={(e) => {
e.currentTarget.style.display = 'none'
}}
/>
</div>
)}
</div>
{error && (
<div className="mt-4 text-[var(--destructive)] text-sm">
{error}
<div className="flex items-center gap-2 p-3 bg-[var(--destructive)]/10 border border-[var(--destructive)]/30 rounded-lg">
<svg className="w-5 h-5 text-[var(--destructive)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-[var(--destructive)] text-sm">{error}</span>
</div>
)}
{success && (
<div className="mt-4 text-[var(--success)] text-sm">
{success}
<div className="flex items-center gap-2 p-3 bg-[var(--success)]/10 border border-[var(--success)]/30 rounded-lg">
<svg className="w-5 h-5 text-[var(--success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-[var(--success)] text-sm">{success}</span>
</div>
)}
<div className="mt-6">
<div className="pt-4">
<Button
type="submit"
disabled={isPending}
className="w-full"
className="w-full bg-[var(--primary)] hover:bg-[var(--primary)]/90 text-[var(--primary-foreground)]"
>
{isPending ? 'Sauvegarde...' : 'Sauvegarder les modifications'}
{isPending ? (
<div className="flex items-center gap-2">
<svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Sauvegarde...
</div>
) : (
<div className="flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Sauvegarder les modifications
</div>
)}
</Button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -36,9 +36,18 @@ export function AuthButton() {
className="p-1 h-auto"
title={`Profil - ${session.user?.email}`}
>
{session.user?.avatar ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={session.user.avatar}
alt="Avatar"
className="w-4 h-4 rounded-full object-cover"
/>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
)}
</Button>
<Button
onClick={() => signOut({ callbackUrl: '/login' })}

View File

@@ -59,12 +59,20 @@ export const authOptions: NextAuthOptions = {
async jwt({ token, user }) {
if (user) {
token.id = user.id
token.firstName = user.firstName
token.lastName = user.lastName
token.avatar = user.avatar
token.role = user.role
}
return token
},
async session({ session, token }) {
if (token && session.user) {
session.user.id = token.id as string
session.user.firstName = token.firstName as string | undefined
session.user.lastName = token.lastName as string | undefined
session.user.avatar = token.avatar as string | undefined
session.user.role = token.role as string
}
return session
},

View File

@@ -28,5 +28,9 @@ declare module "next-auth" {
declare module "next-auth/jwt" {
interface JWT {
id: string
firstName?: string
lastName?: string
avatar?: string
role: string
}
}