feat: adding login/RegisterForm and new columns in PG
This commit is contained in:
@@ -1,11 +1,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { TeamsService, userService } from "@/services";
|
||||
import { AuthService } from "@/services";
|
||||
import {
|
||||
LoginLayout,
|
||||
LoginFormWrapper,
|
||||
LoginLoading,
|
||||
} from "@/components/login";
|
||||
import { LoginLayout, AuthWrapper, LoginLoading } from "@/components/login";
|
||||
|
||||
export default async function LoginPage() {
|
||||
try {
|
||||
@@ -20,19 +16,15 @@ export default async function LoginPage() {
|
||||
const userProfile = await userService.getUserByUuid(userUuid);
|
||||
|
||||
if (userProfile) {
|
||||
// Passer le profil utilisateur pour permettre la modification
|
||||
return (
|
||||
<LoginLayout>
|
||||
<LoginFormWrapper teams={teams} initialUser={userProfile} />
|
||||
</LoginLayout>
|
||||
);
|
||||
// Rediriger vers l'accueil si déjà connecté
|
||||
redirect("/");
|
||||
}
|
||||
}
|
||||
|
||||
// Si l'utilisateur n'est pas connecté, afficher le formulaire de connexion
|
||||
// Si l'utilisateur n'est pas connecté, afficher le formulaire d'auth
|
||||
return (
|
||||
<LoginLayout>
|
||||
<LoginFormWrapper teams={teams} />
|
||||
<AuthWrapper teams={teams} />
|
||||
</LoginLayout>
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
64
components/login/auth-wrapper.tsx
Normal file
64
components/login/auth-wrapper.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { LoginForm, RegisterForm } from "./index";
|
||||
|
||||
interface AuthWrapperProps {
|
||||
teams: any[];
|
||||
}
|
||||
|
||||
export function AuthWrapper({ teams }: AuthWrapperProps) {
|
||||
const [isLogin, setIsLogin] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleLogin = async (email: string, password: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// TODO: Implémenter la logique de login
|
||||
console.log("Login attempt:", { email, password });
|
||||
// await authClient.login(email, password);
|
||||
} catch (error) {
|
||||
console.error("Login failed:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegister = async (data: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
teamId: string;
|
||||
}) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// TODO: Implémenter la logique de register
|
||||
console.log("Register attempt:", data);
|
||||
// await authClient.register(data);
|
||||
} catch (error) {
|
||||
console.error("Register failed:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLogin) {
|
||||
return (
|
||||
<LoginForm
|
||||
onSubmit={handleLogin}
|
||||
onSwitchToRegister={() => setIsLogin(false)}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RegisterForm
|
||||
teams={teams}
|
||||
onSubmit={handleRegister}
|
||||
onSwitchToLogin={() => setIsLogin(true)}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
export { LoginFormWrapper } from "./login-form-wrapper";
|
||||
export { LoginForm } from "./login-form";
|
||||
export { RegisterForm } from "./register-form";
|
||||
export { AuthWrapper } from "./auth-wrapper";
|
||||
export { LoginLoading } from "./login-loading";
|
||||
export { LoginLayout } from "./login-layout";
|
||||
|
||||
117
components/login/login-form.tsx
Normal file
117
components/login/login-form.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Code2 } from "lucide-react";
|
||||
|
||||
interface LoginFormProps {
|
||||
onSubmit: (email: string, password: string) => void;
|
||||
onSwitchToRegister: () => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function LoginForm({
|
||||
onSubmit,
|
||||
onSwitchToRegister,
|
||||
loading = false,
|
||||
}: LoginFormProps) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (email && password) {
|
||||
onSubmit(email, password);
|
||||
}
|
||||
};
|
||||
|
||||
const isValid = email.length > 0 && password.length > 0;
|
||||
|
||||
return (
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 backdrop-blur-sm mb-6">
|
||||
<Code2 className="h-4 w-4 text-blue-400" />
|
||||
<span className="text-sm font-medium text-slate-200">PeakSkills</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl font-bold mb-4 text-white">Connexion</h1>
|
||||
<p className="text-lg text-slate-400 mb-8">
|
||||
Connectez-vous à votre compte PeakSkills
|
||||
</p>
|
||||
|
||||
<div className="max-w-md mx-auto">
|
||||
<Card className="bg-white/5 border-white/10 backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Se connecter</CardTitle>
|
||||
<CardDescription className="text-slate-400">
|
||||
Entrez vos identifiants pour accéder à votre compte
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-slate-300">
|
||||
Email
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="votre@email.com"
|
||||
required
|
||||
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password" className="text-slate-300">
|
||||
Mot de passe
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Votre mot de passe"
|
||||
required
|
||||
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-blue-500 hover:bg-blue-600 text-white"
|
||||
disabled={!isValid || loading}
|
||||
>
|
||||
{loading ? "Connexion..." : "Se connecter"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-white/10">
|
||||
<p className="text-sm text-slate-400">
|
||||
Pas encore de compte ?{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSwitchToRegister}
|
||||
className="text-blue-400 hover:text-blue-300 underline"
|
||||
>
|
||||
Créer un compte
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
327
components/login/register-form.tsx
Normal file
327
components/login/register-form.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useRef, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Team } from "@/lib/types";
|
||||
import { Search, Building2, ChevronDown, Check, Code2 } from "lucide-react";
|
||||
|
||||
interface RegisterFormProps {
|
||||
teams: Team[];
|
||||
onSubmit: (data: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
teamId: string;
|
||||
}) => void;
|
||||
onSwitchToLogin: () => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function RegisterForm({
|
||||
teams,
|
||||
onSubmit,
|
||||
onSwitchToLogin,
|
||||
loading = false,
|
||||
}: RegisterFormProps) {
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [teamId, setTeamId] = useState("");
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [isTeamDropdownOpen, setIsTeamDropdownOpen] = useState(false);
|
||||
const teamDropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [dropdownPosition, setDropdownPosition] = useState<"below" | "above">(
|
||||
"below"
|
||||
);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (password !== confirmPassword) {
|
||||
alert("Les mots de passe ne correspondent pas");
|
||||
return;
|
||||
}
|
||||
if (firstName && lastName && email && password && teamId) {
|
||||
onSubmit({ firstName, lastName, email, password, teamId });
|
||||
}
|
||||
};
|
||||
|
||||
const isValid =
|
||||
firstName.length > 0 &&
|
||||
lastName.length > 0 &&
|
||||
email.length > 0 &&
|
||||
password.length > 0 &&
|
||||
password === confirmPassword &&
|
||||
teamId.length > 0;
|
||||
|
||||
// Group teams by direction and filter by search term
|
||||
const teamsByDirection = useMemo(() => {
|
||||
const filteredTeams = teams.filter(
|
||||
(team) =>
|
||||
team.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
team.direction.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
return filteredTeams.reduce((acc, team) => {
|
||||
if (!acc[team.direction]) {
|
||||
acc[team.direction] = [];
|
||||
}
|
||||
acc[team.direction].push(team);
|
||||
return acc;
|
||||
}, {} as Record<string, Team[]>);
|
||||
}, [teams, searchTerm]);
|
||||
|
||||
// Calculate dropdown position when opening
|
||||
const handleDropdownToggle = () => {
|
||||
if (!isTeamDropdownOpen && teamDropdownRef.current) {
|
||||
const rect = teamDropdownRef.current.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
const spaceBelow = viewportHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
const dropdownHeight = 300;
|
||||
|
||||
setDropdownPosition(
|
||||
spaceBelow >= dropdownHeight || spaceBelow > spaceAbove
|
||||
? "below"
|
||||
: "above"
|
||||
);
|
||||
}
|
||||
setIsTeamDropdownOpen(!isTeamDropdownOpen);
|
||||
};
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
teamDropdownRef.current &&
|
||||
!teamDropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsTeamDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const selectedTeam = teams.find((team) => team.id === teamId);
|
||||
|
||||
return (
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 backdrop-blur-sm mb-6">
|
||||
<Code2 className="h-4 w-4 text-blue-400" />
|
||||
<span className="text-sm font-medium text-slate-200">PeakSkills</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl font-bold mb-4 text-white">Créer un compte</h1>
|
||||
<p className="text-lg text-slate-400 mb-8">
|
||||
Rejoignez PeakSkills et commencez votre évaluation
|
||||
</p>
|
||||
|
||||
<div className="max-w-md mx-auto">
|
||||
<Card className="bg-white/5 border-white/10 backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Inscription</CardTitle>
|
||||
<CardDescription className="text-slate-400">
|
||||
Créez votre compte pour commencer
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="firstName" className="text-slate-300">
|
||||
Prénom
|
||||
</Label>
|
||||
<Input
|
||||
id="firstName"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
placeholder="Votre prénom"
|
||||
required
|
||||
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lastName" className="text-slate-300">
|
||||
Nom
|
||||
</Label>
|
||||
<Input
|
||||
id="lastName"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
placeholder="Votre nom"
|
||||
required
|
||||
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-slate-300">
|
||||
Email
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="votre@email.com"
|
||||
required
|
||||
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password" className="text-slate-300">
|
||||
Mot de passe
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Mot de passe"
|
||||
required
|
||||
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword" className="text-slate-300">
|
||||
Confirmer
|
||||
</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="Confirmer"
|
||||
required
|
||||
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="team" className="text-slate-300">
|
||||
Équipe
|
||||
</Label>
|
||||
<div className="relative" ref={teamDropdownRef}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full justify-between bg-white/10 border-white/20 text-white hover:bg-white/20"
|
||||
onClick={handleDropdownToggle}
|
||||
>
|
||||
<span
|
||||
className={selectedTeam ? "text-white" : "text-slate-400"}
|
||||
>
|
||||
{selectedTeam
|
||||
? selectedTeam.name
|
||||
: "Sélectionnez votre équipe"}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{isTeamDropdownOpen && (
|
||||
<div
|
||||
className={`absolute left-0 right-0 bg-gray-800 border border-white/20 rounded-md shadow-lg z-50 max-h-[300px] overflow-y-auto ${
|
||||
dropdownPosition === "below"
|
||||
? "top-full mt-1"
|
||||
: "bottom-full mb-1"
|
||||
}`}
|
||||
>
|
||||
<div className="sticky top-0 bg-gray-800 border-b border-white/20 p-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-slate-400" />
|
||||
<Input
|
||||
placeholder="Rechercher une équipe..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-8 h-8 text-sm bg-gray-700 border-white/20 text-white"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Object.entries(teamsByDirection).map(
|
||||
([direction, directionTeams]) => (
|
||||
<div key={direction}>
|
||||
<div className="px-3 py-2 text-sm font-semibold text-slate-400 bg-gray-700/50 border-b border-white/20 flex items-center gap-2">
|
||||
<Building2 className="h-3 w-3" />
|
||||
{direction}
|
||||
</div>
|
||||
{directionTeams.map((team) => (
|
||||
<button
|
||||
key={team.id}
|
||||
type="button"
|
||||
className={`w-full px-3 py-2 text-left hover:bg-gray-700/50 flex items-center justify-between text-white ${
|
||||
team.id === teamId ? "bg-blue-600/20" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setTeamId(team.id);
|
||||
setIsTeamDropdownOpen(false);
|
||||
setSearchTerm("");
|
||||
}}
|
||||
>
|
||||
<span>{team.name}</span>
|
||||
{team.id === teamId && (
|
||||
<Check className="h-4 w-4 text-blue-400" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{Object.keys(teamsByDirection).length === 0 && (
|
||||
<div className="px-3 py-4 text-center text-sm text-slate-400">
|
||||
Aucune équipe trouvée pour "{searchTerm}"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-blue-500 hover:bg-blue-600 text-white"
|
||||
disabled={!isValid || loading}
|
||||
>
|
||||
{loading ? "Création..." : "Créer le compte"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-white/10">
|
||||
<p className="text-sm text-slate-400">
|
||||
Déjà un compte ?{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSwitchToLogin}
|
||||
className="text-blue-400 hover:text-blue-300 underline"
|
||||
>
|
||||
Se connecter
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
package.json
10
package.json
@@ -44,7 +44,10 @@
|
||||
"@radix-ui/react-toggle": "1.1.1",
|
||||
"@radix-ui/react-toggle-group": "1.1.1",
|
||||
"@radix-ui/react-tooltip": "1.1.6",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/pg": "^8.11.10",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
@@ -55,6 +58,7 @@
|
||||
"lucide-react": "^0.454.0",
|
||||
"next": "15.2.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"pg": "^8.12.0",
|
||||
"react": "^19",
|
||||
"react-day-picker": "9.8.0",
|
||||
"react-dom": "^19",
|
||||
@@ -65,9 +69,7 @@
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.9.9",
|
||||
"zod": "3.25.67",
|
||||
"pg": "^8.12.0",
|
||||
"@types/pg": "^8.11.10"
|
||||
"zod": "3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.9",
|
||||
@@ -76,8 +78,8 @@
|
||||
"@types/react-dom": "^19",
|
||||
"postcss": "^8.5",
|
||||
"tailwindcss": "^4.1.9",
|
||||
"tw-animate-css": "1.3.3",
|
||||
"tsx": "^4.19.2",
|
||||
"tw-animate-css": "1.3.3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -104,12 +104,18 @@ importers:
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: 1.1.6
|
||||
version: 1.1.6(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@types/bcrypt':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
'@types/pg':
|
||||
specifier: ^8.11.10
|
||||
version: 8.15.5
|
||||
autoprefixer:
|
||||
specifier: ^10.4.20
|
||||
version: 10.4.21(postcss@8.5.6)
|
||||
bcrypt:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@@ -1345,6 +1351,9 @@ packages:
|
||||
'@tailwindcss/postcss@4.1.12':
|
||||
resolution: {integrity: sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==}
|
||||
|
||||
'@types/bcrypt@6.0.0':
|
||||
resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==}
|
||||
|
||||
'@types/d3-array@3.2.1':
|
||||
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
|
||||
|
||||
@@ -1397,6 +1406,10 @@ packages:
|
||||
peerDependencies:
|
||||
postcss: ^8.1.0
|
||||
|
||||
bcrypt@6.0.0:
|
||||
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
browserslist@4.25.3:
|
||||
resolution: {integrity: sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
@@ -1712,6 +1725,14 @@ packages:
|
||||
sass:
|
||||
optional: true
|
||||
|
||||
node-addon-api@8.5.0:
|
||||
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
|
||||
engines: {node: ^18 || ^20 || >= 21}
|
||||
|
||||
node-gyp-build@4.8.4:
|
||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||
hasBin: true
|
||||
|
||||
node-releases@2.0.19:
|
||||
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
||||
|
||||
@@ -3042,6 +3063,10 @@ snapshots:
|
||||
postcss: 8.5.6
|
||||
tailwindcss: 4.1.12
|
||||
|
||||
'@types/bcrypt@6.0.0':
|
||||
dependencies:
|
||||
'@types/node': 22.17.2
|
||||
|
||||
'@types/d3-array@3.2.1': {}
|
||||
|
||||
'@types/d3-color@3.1.3': {}
|
||||
@@ -3098,6 +3123,11 @@ snapshots:
|
||||
postcss: 8.5.6
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
bcrypt@6.0.0:
|
||||
dependencies:
|
||||
node-addon-api: 8.5.0
|
||||
node-gyp-build: 4.8.4
|
||||
|
||||
browserslist@4.25.3:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001735
|
||||
@@ -3392,6 +3422,10 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
node-addon-api@8.5.0: {}
|
||||
|
||||
node-gyp-build@4.8.4: {}
|
||||
|
||||
node-releases@2.0.19: {}
|
||||
|
||||
normalize-range@0.1.2: {}
|
||||
|
||||
@@ -49,10 +49,11 @@ CREATE TABLE users (
|
||||
first_name VARCHAR(100) NOT NULL,
|
||||
last_name VARCHAR(100) NOT NULL,
|
||||
team_id VARCHAR(50) REFERENCES teams(id),
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (uuid_id),
|
||||
UNIQUE(first_name, last_name, team_id),
|
||||
UNIQUE(uuid_id)
|
||||
);
|
||||
|
||||
@@ -89,7 +90,8 @@ CREATE INDEX idx_teams_direction ON teams(direction);
|
||||
CREATE INDEX idx_skills_category_id ON skills(category_id);
|
||||
CREATE INDEX idx_skill_links_skill_id ON skill_links(skill_id);
|
||||
CREATE INDEX idx_users_team_id ON users(team_id);
|
||||
CREATE INDEX idx_users_unique_person ON users(first_name, last_name, team_id);
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
|
||||
CREATE INDEX idx_users_uuid_id ON users(uuid_id); -- Index on UUID for performance
|
||||
CREATE INDEX idx_user_evaluations_user_uuid ON user_evaluations(user_uuid);
|
||||
CREATE INDEX idx_user_evaluations_user_id ON user_evaluations(user_id); -- Legacy index
|
||||
|
||||
22
scripts/migrations/add_auth_fields.sql
Normal file
22
scripts/migrations/add_auth_fields.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Ajout des champs email et password à la table users
|
||||
-- Étape 1: Ajouter les colonnes sans contraintes NOT NULL
|
||||
ALTER TABLE users
|
||||
ADD COLUMN email VARCHAR(255),
|
||||
ADD COLUMN password_hash VARCHAR(255);
|
||||
|
||||
-- Étape 2: Remplir les colonnes avec des valeurs par défaut pour les données existantes
|
||||
UPDATE users
|
||||
SET
|
||||
email = CONCAT(LOWER(first_name), '.', LOWER(last_name), '@peakskills.local'),
|
||||
password_hash = '$2b$10$default.hash.for.existing.users.placeholder'
|
||||
WHERE email IS NULL;
|
||||
|
||||
-- Étape 3: Ajouter les contraintes NOT NULL après avoir rempli les données
|
||||
ALTER TABLE users
|
||||
ALTER COLUMN email SET NOT NULL,
|
||||
ALTER COLUMN password_hash SET NOT NULL;
|
||||
|
||||
-- Étape 4: Ajouter l'index et la contrainte unique sur l'email
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email);
|
||||
|
||||
Reference in New Issue
Block a user