First Push

This commit is contained in:
2026-01-20 17:20:13 +01:00
commit 66f73c3523
24 changed files with 7560 additions and 0 deletions

39
.gitignore vendored Normal file
View 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
View 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
View 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 !

View 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 }
);
}
}

View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function Home() {
redirect("/login");
}

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig

6807
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View 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
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
prisma/dev.db Normal file

Binary file not shown.

20
prisma/schema.prisma Normal file
View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 129 KiB

43
scripts/setup.ts Normal file
View 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
View 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
View 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"]
}