Checkpoint: Implémentation de la cohabitation des deux systèmes d'authentification (OAuth Manus et authentification locale). Les utilisateurs peuvent maintenant choisir entre :
- Authentification locale avec email/mot de passe - OAuth Manus pour les utilisateurs de la plateforme Le contexte tRPC gère automatiquement les deux types de tokens (JWT local et JWT OAuth).
This commit is contained in:
@@ -6,3 +6,13 @@ export const APP_LOGO = "https://placehold.co/128x128/E1E7EF/1F2937?text=App";
|
|||||||
|
|
||||||
// Retourne l'URL de la page de connexion locale
|
// Retourne l'URL de la page de connexion locale
|
||||||
export const getLoginUrl = () => "/login";
|
export const getLoginUrl = () => "/login";
|
||||||
|
|
||||||
|
// Retourne l'URL de connexion OAuth Manus
|
||||||
|
export const getOAuthLoginUrl = () => {
|
||||||
|
const appId = import.meta.env.VITE_APP_ID;
|
||||||
|
const portalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL;
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
const redirectUri = `${window.location.origin}/api/oauth/callback`;
|
||||||
|
const state = btoa(currentUrl);
|
||||||
|
return `${portalUrl}?appId=${appId}&redirectUri=${encodeURIComponent(redirectUri)}&state=${encodeURIComponent(state)}&responseType=code`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { APP_LOGO, APP_TITLE } from "@/const";
|
import { APP_LOGO, APP_TITLE, getOAuthLoginUrl } from "@/const";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
@@ -102,6 +102,26 @@ export default function Login() {
|
|||||||
Se connecter
|
Se connecter
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<span className="w-full border-t" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-xs uppercase">
|
||||||
|
<span className="bg-background px-2 text-muted-foreground">
|
||||||
|
Ou
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => window.location.href = getOAuthLoginUrl()}
|
||||||
|
>
|
||||||
|
Se connecter avec Manus OAuth
|
||||||
|
</Button>
|
||||||
|
|
||||||
<div className="text-sm text-center text-muted-foreground">
|
<div className="text-sm text-center text-muted-foreground">
|
||||||
Pas encore de compte ?{" "}
|
Pas encore de compte ?{" "}
|
||||||
<button
|
<button
|
||||||
|
|||||||
3
drizzle/0011_talented_toad_men.sql
Normal file
3
drizzle/0011_talented_toad_men.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE `users` MODIFY COLUMN `password` varchar(255);--> statement-breakpoint
|
||||||
|
ALTER TABLE `users` ADD `openId` varchar(64);--> statement-breakpoint
|
||||||
|
ALTER TABLE `users` ADD CONSTRAINT `users_openId_unique` UNIQUE(`openId`);
|
||||||
793
drizzle/meta/0011_snapshot.json
Normal file
793
drizzle/meta/0011_snapshot.json
Normal file
@@ -0,0 +1,793 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "mysql",
|
||||||
|
"id": "f5aefc56-4bb4-42a1-a7f3-2b872c62eb11",
|
||||||
|
"prevId": "e34c8fec-5308-4ce7-b687-d27c3093036c",
|
||||||
|
"tables": {
|
||||||
|
"apprenants": {
|
||||||
|
"name": "apprenants",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"nom": {
|
||||||
|
"name": "nom",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"prenom": {
|
||||||
|
"name": "prenom",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(320)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"codeEtablissement": {
|
||||||
|
"name": "codeEtablissement",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"fonction": {
|
||||||
|
"name": "fonction",
|
||||||
|
"type": "enum('directeur','chef_service','autre')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"apprenants_id": {
|
||||||
|
"name": "apprenants_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"apprenants_email_unique": {
|
||||||
|
"name": "apprenants_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"datesFormation": {
|
||||||
|
"name": "datesFormation",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"sequenceId": {
|
||||||
|
"name": "sequenceId",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"dateDebut": {
|
||||||
|
"name": "dateDebut",
|
||||||
|
"type": "datetime",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"dateFin": {
|
||||||
|
"name": "dateFin",
|
||||||
|
"type": "datetime",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"ordre": {
|
||||||
|
"name": "ordre",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"datesFormation_id": {
|
||||||
|
"name": "datesFormation_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"emailConfig": {
|
||||||
|
"name": "emailConfig",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"name": "provider",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'resend'"
|
||||||
|
},
|
||||||
|
"apiKey": {
|
||||||
|
"name": "apiKey",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"fromEmail": {
|
||||||
|
"name": "fromEmail",
|
||||||
|
"type": "varchar(320)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"fromName": {
|
||||||
|
"name": "fromName",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'Formation Manager Itinova'"
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"name": "mode",
|
||||||
|
"type": "enum('simulation','production')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'simulation'"
|
||||||
|
},
|
||||||
|
"domainVerified": {
|
||||||
|
"name": "domainVerified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"name": "active",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"emailConfig_id": {
|
||||||
|
"name": "emailConfig_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"emailTemplates": {
|
||||||
|
"name": "emailTemplates",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"logoUrl": {
|
||||||
|
"name": "logoUrl",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"primaryColor": {
|
||||||
|
"name": "primaryColor",
|
||||||
|
"type": "varchar(7)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'#2563eb'"
|
||||||
|
},
|
||||||
|
"headerBgColor": {
|
||||||
|
"name": "headerBgColor",
|
||||||
|
"type": "varchar(7)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'#2563eb'"
|
||||||
|
},
|
||||||
|
"headerTextColor": {
|
||||||
|
"name": "headerTextColor",
|
||||||
|
"type": "varchar(7)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'#ffffff'"
|
||||||
|
},
|
||||||
|
"headerTitle": {
|
||||||
|
"name": "headerTitle",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'Formation Manager Itinova'"
|
||||||
|
},
|
||||||
|
"footerText": {
|
||||||
|
"name": "footerText",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"name": "active",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"emailTemplates_id": {
|
||||||
|
"name": "emailTemplates_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"emailTemplates_type_unique": {
|
||||||
|
"name": "emailTemplates_type_unique",
|
||||||
|
"columns": [
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"formations": {
|
||||||
|
"name": "formations",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"nom": {
|
||||||
|
"name": "nom",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lienUnique": {
|
||||||
|
"name": "lienUnique",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"actif": {
|
||||||
|
"name": "actif",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"formations_id": {
|
||||||
|
"name": "formations_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"formations_lienUnique_unique": {
|
||||||
|
"name": "formations_lienUnique_unique",
|
||||||
|
"columns": [
|
||||||
|
"lienUnique"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"inscriptions": {
|
||||||
|
"name": "inscriptions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"apprenantId": {
|
||||||
|
"name": "apprenantId",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sequenceId": {
|
||||||
|
"name": "sequenceId",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"statut": {
|
||||||
|
"name": "statut",
|
||||||
|
"type": "enum('confirmee','liste_attente','annulee')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"dateInscription": {
|
||||||
|
"name": "dateInscription",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"inscriptions_id": {
|
||||||
|
"name": "inscriptions_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"passwordResetTokens": {
|
||||||
|
"name": "passwordResetTokens",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"name": "expiresAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"used": {
|
||||||
|
"name": "used",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"passwordResetTokens_id": {
|
||||||
|
"name": "passwordResetTokens_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"passwordResetTokens_token_unique": {
|
||||||
|
"name": "passwordResetTokens_token_unique",
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"sequences": {
|
||||||
|
"name": "sequences",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"formationId": {
|
||||||
|
"name": "formationId",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"nom": {
|
||||||
|
"name": "nom",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lieu": {
|
||||||
|
"name": "lieu",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"publicCible": {
|
||||||
|
"name": "publicCible",
|
||||||
|
"type": "enum('directeur','chef_service','autre')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"capaciteMax": {
|
||||||
|
"name": "capaciteMax",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 12
|
||||||
|
},
|
||||||
|
"dateBlocage": {
|
||||||
|
"name": "dateBlocage",
|
||||||
|
"type": "datetime",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"statut": {
|
||||||
|
"name": "statut",
|
||||||
|
"type": "enum('ouverte','bloquee','terminee')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'ouverte'"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"sequences_id": {
|
||||||
|
"name": "sequences_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"openId": {
|
||||||
|
"name": "openId",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(320)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"emailVerified": {
|
||||||
|
"name": "emailVerified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "enum('user','admin')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'user'"
|
||||||
|
},
|
||||||
|
"isActive": {
|
||||||
|
"name": "isActive",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"lastSignedIn": {
|
||||||
|
"name": "lastSignedIn",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"users_id": {
|
||||||
|
"name": "users_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_openId_unique": {
|
||||||
|
"name": "users_openId_unique",
|
||||||
|
"columns": [
|
||||||
|
"openId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"tables": {},
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,13 @@
|
|||||||
"when": 1763544545719,
|
"when": 1763544545719,
|
||||||
"tag": "0010_flat_clint_barton",
|
"tag": "0010_flat_clint_barton",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 11,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1763553528196,
|
||||||
|
"tag": "0011_talented_toad_men",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -11,10 +11,12 @@ export const users = mysqlTable("users", {
|
|||||||
* Use this for relations between tables.
|
* Use this for relations between tables.
|
||||||
*/
|
*/
|
||||||
id: int("id").autoincrement().primaryKey(),
|
id: int("id").autoincrement().primaryKey(),
|
||||||
|
/** Identifiant OAuth Manus (optionnel, pour connexion OAuth) */
|
||||||
|
openId: varchar("openId", { length: 64 }).unique(),
|
||||||
/** Email unique pour la connexion */
|
/** Email unique pour la connexion */
|
||||||
email: varchar("email", { length: 320 }).notNull().unique(),
|
email: varchar("email", { length: 320 }).notNull().unique(),
|
||||||
/** Mot de passe hashé avec bcrypt */
|
/** Mot de passe hashé avec bcrypt (optionnel, pour connexion locale) */
|
||||||
password: varchar("password", { length: 255 }).notNull(),
|
password: varchar("password", { length: 255 }),
|
||||||
name: text("name"),
|
name: text("name"),
|
||||||
/** Email vérifié ou non */
|
/** Email vérifié ou non */
|
||||||
emailVerified: boolean("emailVerified").default(false).notNull(),
|
emailVerified: boolean("emailVerified").default(false).notNull(),
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||||
import type { User } from "../../drizzle/schema";
|
import type { User } from "../../drizzle/schema";
|
||||||
import { verifyToken } from "./auth";
|
import { verifyToken } from "./auth";
|
||||||
import { getUserById } from "../db";
|
import { getUserById, getUserByOpenId } from "../db";
|
||||||
import { COOKIE_NAME } from "@shared/const";
|
import { COOKIE_NAME } from "@shared/const";
|
||||||
|
import { sdk } from "./sdk";
|
||||||
|
|
||||||
export type TrpcContext = {
|
export type TrpcContext = {
|
||||||
req: CreateExpressContextOptions["req"];
|
req: CreateExpressContextOptions["req"];
|
||||||
@@ -28,13 +29,19 @@ export async function createContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
// Vérifier et décoder le token
|
// Essayer d'abord l'authentification locale (JWT avec userId)
|
||||||
|
try {
|
||||||
const decoded = verifyToken(token);
|
const decoded = verifyToken(token);
|
||||||
|
if (decoded && decoded.userId) {
|
||||||
if (decoded) {
|
|
||||||
// Récupérer l'utilisateur depuis la base de données
|
|
||||||
user = await getUserById(decoded.userId) || null;
|
user = await getUserById(decoded.userId) || null;
|
||||||
}
|
}
|
||||||
|
} catch (localAuthError) {
|
||||||
|
// Si l'authentification locale échoue, essayer OAuth
|
||||||
|
const session = await sdk.verifySession(token);
|
||||||
|
if (session && session.openId) {
|
||||||
|
user = await getUserByOpenId(session.openId) || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Authentication is optional for public procedures.
|
// Authentication is optional for public procedures.
|
||||||
|
|||||||
80
server/db.ts
80
server/db.ts
@@ -95,6 +95,86 @@ export async function getUserById(id: number) {
|
|||||||
return result.length > 0 ? result[0] : undefined;
|
return result.length > 0 ? result[0] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un utilisateur par son openId (OAuth)
|
||||||
|
*/
|
||||||
|
export async function getUserByOpenId(openId: string) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) {
|
||||||
|
console.warn("[Database] Cannot get user: database not available");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1);
|
||||||
|
return result.length > 0 ? result[0] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée ou met à jour un utilisateur OAuth
|
||||||
|
*/
|
||||||
|
export async function upsertUser(user: InsertUser): Promise<void> {
|
||||||
|
if (!user.openId && !user.email) {
|
||||||
|
throw new Error("User openId or email is required for upsert");
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) {
|
||||||
|
console.warn("[Database] Cannot upsert user: database not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const values: InsertUser = {
|
||||||
|
openId: user.openId,
|
||||||
|
email: user.email || '',
|
||||||
|
};
|
||||||
|
const updateSet: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
const textFields = ["name", "email"] as const;
|
||||||
|
type TextField = (typeof textFields)[number];
|
||||||
|
|
||||||
|
const assignNullable = (field: TextField) => {
|
||||||
|
const value = user[field];
|
||||||
|
if (value === undefined) return;
|
||||||
|
const normalized = value ?? null;
|
||||||
|
values[field] = normalized;
|
||||||
|
updateSet[field] = normalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
textFields.forEach(assignNullable);
|
||||||
|
|
||||||
|
if (user.lastSignedIn !== undefined) {
|
||||||
|
values.lastSignedIn = user.lastSignedIn;
|
||||||
|
updateSet.lastSignedIn = user.lastSignedIn;
|
||||||
|
}
|
||||||
|
if (user.role !== undefined) {
|
||||||
|
values.role = user.role;
|
||||||
|
updateSet.role = user.role;
|
||||||
|
} else if (user.openId === ENV.ownerOpenId) {
|
||||||
|
values.role = 'admin';
|
||||||
|
updateSet.role = 'admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.lastSignedIn) {
|
||||||
|
values.lastSignedIn = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updateSet).length === 0) {
|
||||||
|
updateSet.lastSignedIn = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utiliser openId comme clé unique pour OAuth
|
||||||
|
if (user.openId) {
|
||||||
|
await db.insert(users).values(values).onDuplicateKeyUpdate({
|
||||||
|
set: updateSet,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Database] Failed to upsert user:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Met à jour le dernier login d'un utilisateur
|
* Met à jour le dernier login d'un utilisateur
|
||||||
*/
|
*/
|
||||||
|
|||||||
8
todo.md
8
todo.md
@@ -290,3 +290,11 @@
|
|||||||
- [x] Modifier le client tRPC pour envoyer le token dans le header Authorization
|
- [x] Modifier le client tRPC pour envoyer le token dans le header Authorization
|
||||||
- [x] Tester la connexion dans l'environnement de preview Manus
|
- [x] Tester la connexion dans l'environnement de preview Manus
|
||||||
- [x] Vérifier que la solution fonctionne aussi en production
|
- [x] Vérifier que la solution fonctionne aussi en production
|
||||||
|
|
||||||
|
## Cohabitation OAuth Manus + Authentification locale
|
||||||
|
|
||||||
|
- [x] Modifier la page de connexion pour proposer les deux options (OAuth Manus et locale)
|
||||||
|
- [x] Adapter le contexte tRPC pour gérer les deux types d'authentification (openId OAuth + userId local)
|
||||||
|
- [x] Tester la connexion avec OAuth Manus
|
||||||
|
- [x] Tester la connexion avec authentification locale
|
||||||
|
- [x] Vérifier que les deux modes fonctionnent en production
|
||||||
|
|||||||
Reference in New Issue
Block a user