First Push
This commit is contained in:
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# prisma
|
||||
/prisma/migrations
|
||||
71
README.md
Normal file
71
README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Platform SaaS
|
||||
|
||||
Plateforme SaaS développée avec Next.js, Tailwind CSS et Prisma.
|
||||
|
||||
## Stack technique
|
||||
|
||||
- **Next.js 14** - Framework React avec App Router
|
||||
- **TypeScript** - Typage statique
|
||||
- **Tailwind CSS** - Framework CSS utilitaire
|
||||
- **Prisma** - ORM pour la base de données
|
||||
- **SQLite** - Base de données (développement)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Installer les dépendances :
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Configurer les variables d'environnement :
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. Initialiser la base de données :
|
||||
```bash
|
||||
npm run db:push
|
||||
```
|
||||
|
||||
4. Générer le client Prisma :
|
||||
```bash
|
||||
npm run db:generate
|
||||
```
|
||||
|
||||
5. Lancer le serveur de développement :
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
L'application sera accessible sur [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
## Structure du projet
|
||||
|
||||
```
|
||||
platform/
|
||||
├── app/ # Pages et routes Next.js
|
||||
│ ├── login/ # Page de connexion
|
||||
│ ├── dashboard/ # Page tableau de bord
|
||||
│ └── api/ # Routes API
|
||||
├── components/ # Composants React
|
||||
├── lib/ # Utilitaires et helpers
|
||||
├── prisma/ # Schéma Prisma
|
||||
└── public/ # Fichiers statiques
|
||||
```
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
- ✅ Page de connexion
|
||||
- ✅ Page tableau de bord
|
||||
- ✅ Authentification basique avec sessions
|
||||
- ✅ Protection des routes
|
||||
- ✅ Interface responsive avec Tailwind CSS
|
||||
|
||||
## Commandes disponibles
|
||||
|
||||
- `npm run dev` - Lancer le serveur de développement
|
||||
- `npm run build` - Construire l'application pour la production
|
||||
- `npm run start` - Lancer l'application en production
|
||||
- `npm run db:push` - Synchroniser le schéma Prisma avec la base de données
|
||||
- `npm run db:studio` - Ouvrir Prisma Studio (interface graphique pour la DB)
|
||||
- `npm run db:generate` - Générer le client Prisma
|
||||
54
SETUP.md
Normal file
54
SETUP.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Guide de configuration
|
||||
|
||||
## Étapes d'installation
|
||||
|
||||
1. **Installer les dépendances**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Créer le fichier `.env`**
|
||||
Créez un fichier `.env` à la racine du projet avec le contenu suivant :
|
||||
```
|
||||
DATABASE_URL="file:./dev.db"
|
||||
NEXTAUTH_SECRET="votre-cle-secrete-changez-en-production"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
```
|
||||
|
||||
3. **Initialiser la base de données**
|
||||
```bash
|
||||
npm run db:push
|
||||
```
|
||||
|
||||
4. **Générer le client Prisma**
|
||||
```bash
|
||||
npm run db:generate
|
||||
```
|
||||
|
||||
5. **Créer un utilisateur de test**
|
||||
```bash
|
||||
npm run setup
|
||||
```
|
||||
|
||||
Cela créera un utilisateur avec :
|
||||
- Email: `admin@example.com`
|
||||
- Mot de passe: `admin123`
|
||||
|
||||
6. **Lancer le serveur de développement**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
L'application sera accessible sur [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
## Accès
|
||||
|
||||
- Page de connexion: http://localhost:3000/login
|
||||
- Tableau de bord: http://localhost:3000/dashboard (nécessite une connexion)
|
||||
|
||||
## Utilisateur de test
|
||||
|
||||
- **Email**: admin@example.com
|
||||
- **Mot de passe**: admin123
|
||||
|
||||
⚠️ **Important**: Changez ces identifiants en production !
|
||||
41
app/api/auth/login/route.ts
Normal file
41
app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { login, setSession } from '@/lib/auth';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { email, password } = await request.json();
|
||||
|
||||
if (!email || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email et mot de passe requis' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const user = await login(email, password);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email ou mot de passe incorrect' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
await setSession(user.id);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Une erreur est survenue' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
7
app/api/auth/logout/route.ts
Normal file
7
app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { clearSession } from '@/lib/auth';
|
||||
|
||||
export async function POST() {
|
||||
await clearSession();
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
50
app/dashboard/page.tsx
Normal file
50
app/dashboard/page.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getCurrentUser } from '@/lib/auth';
|
||||
import DashboardLayout from '@/components/DashboardLayout';
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const user = await getCurrentUser();
|
||||
|
||||
if (!user) {
|
||||
redirect('/login');
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardLayout user={user}>
|
||||
<div className="p-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
Tableau de bord
|
||||
</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Bienvenue
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{user.name || user.email}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Statistiques
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Contenu à venir
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Activité récente
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Contenu à venir
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
27
app/globals.css
Normal file
27
app/globals.css
Normal file
@@ -0,0 +1,27 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
19
app/layout.tsx
Normal file
19
app/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Platform SaaS",
|
||||
description: "Plateforme SaaS",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="fr">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
27
app/login/page.tsx
Normal file
27
app/login/page.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getCurrentUser, login } from '@/lib/auth';
|
||||
import LoginForm from '@/components/LoginForm';
|
||||
|
||||
export default async function LoginPage() {
|
||||
const user = await getCurrentUser();
|
||||
|
||||
if (user) {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
||||
<div className="max-w-md w-full space-y-8 p-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||
Connexion à votre compte
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
Accédez à votre tableau de bord
|
||||
</p>
|
||||
</div>
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
app/page.tsx
Normal file
5
app/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Home() {
|
||||
redirect("/login");
|
||||
}
|
||||
65
components/DashboardLayout.tsx
Normal file
65
components/DashboardLayout.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
user: User;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function DashboardLayout({ user, children }: DashboardLayoutProps) {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleLogout = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await fetch('/api/auth/logout', { method: 'POST' });
|
||||
router.push('/login');
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<nav className="bg-white dark:bg-gray-800 shadow">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex items-center">
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
Platform SaaS
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{user.name || user.email}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-md disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Déconnexion...' : 'Déconnexion'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
97
components/LoginForm.tsx
Normal file
97
components/LoginForm.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function LoginForm() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(data.error || 'Une erreur est survenue');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
router.push('/dashboard');
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
setError('Une erreur est survenue lors de la connexion');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 px-4 py-3 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label htmlFor="email" className="sr-only">
|
||||
Adresse email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white bg-white dark:bg-gray-800 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Adresse email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
Mot de passe
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white bg-white dark:bg-gray-800 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Mot de passe"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Connexion...' : 'Se connecter'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
75
lib/auth.ts
Normal file
75
lib/auth.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { prisma } from './prisma';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, 10);
|
||||
}
|
||||
|
||||
export async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
|
||||
return bcrypt.compare(password, hashedPassword);
|
||||
}
|
||||
|
||||
export async function createUser(email: string, password: string, name?: string) {
|
||||
const hashedPassword = await hashPassword(password);
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
password: hashedPassword,
|
||||
name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserByEmail(email: string) {
|
||||
return prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
}
|
||||
|
||||
export async function login(email: string, password: string) {
|
||||
const user = await getUserByEmail(email);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValid = await verifyPassword(password, user.password);
|
||||
if (!isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function getCurrentUser() {
|
||||
const cookieStore = await cookies();
|
||||
const userId = cookieStore.get('user_id')?.value;
|
||||
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function setSession(userId: string) {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set('user_id', userId, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearSession() {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete('user_id');
|
||||
}
|
||||
9
lib/prisma.ts
Normal file
9
lib/prisma.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined
|
||||
}
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
||||
6
next.config.js
Normal file
6
next.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
6807
package-lock.json
generated
Normal file
6807
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
package.json
Normal file
37
package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "platform",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"db:push": "prisma db push",
|
||||
"db:studio": "prisma studio",
|
||||
"db:generate": "prisma generate",
|
||||
"setup": "tsx scripts/setup.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.19.1",
|
||||
"next": "14.2.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"next-auth": "^4.24.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "^20.14.12",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "14.2.5",
|
||||
"postcss": "^8.4.40",
|
||||
"prisma": "^5.19.1",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
prisma/dev.db
Normal file
BIN
prisma/dev.db
Normal file
Binary file not shown.
20
prisma/schema.prisma
Normal file
20
prisma/schema.prisma
Normal file
@@ -0,0 +1,20 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
password String
|
||||
name String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
9
public/logo.svg
Normal file
9
public/logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 129 KiB |
43
scripts/setup.ts
Normal file
43
scripts/setup.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Initialisation de la base de données...');
|
||||
|
||||
// Créer un utilisateur de test
|
||||
const email = 'admin@example.com';
|
||||
const password = 'admin123';
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
console.log('✅ Utilisateur de test existe déjà');
|
||||
console.log(` Email: ${email}`);
|
||||
console.log(` Mot de passe: ${password}`);
|
||||
} else {
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
password: hashedPassword,
|
||||
name: 'Administrateur',
|
||||
},
|
||||
});
|
||||
console.log('✅ Utilisateur de test créé');
|
||||
console.log(` Email: ${email}`);
|
||||
console.log(` Mot de passe: ${password}`);
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('❌ Erreur:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
19
tailwind.config.ts
Normal file
19
tailwind.config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: "var(--background)",
|
||||
foreground: "var(--foreground)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
||||
27
tsconfig.json
Normal file
27
tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user