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
|
||||
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 { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
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";
|
||||
|
||||
export default function Login() {
|
||||
@@ -102,6 +102,26 @@ export default function Login() {
|
||||
Se connecter
|
||||
</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">
|
||||
Pas encore de compte ?{" "}
|
||||
<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,
|
||||
"tag": "0010_flat_clint_barton",
|
||||
"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.
|
||||
*/
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
/** Identifiant OAuth Manus (optionnel, pour connexion OAuth) */
|
||||
openId: varchar("openId", { length: 64 }).unique(),
|
||||
/** Email unique pour la connexion */
|
||||
email: varchar("email", { length: 320 }).notNull().unique(),
|
||||
/** Mot de passe hashé avec bcrypt */
|
||||
password: varchar("password", { length: 255 }).notNull(),
|
||||
/** Mot de passe hashé avec bcrypt (optionnel, pour connexion locale) */
|
||||
password: varchar("password", { length: 255 }),
|
||||
name: text("name"),
|
||||
/** Email vérifié ou non */
|
||||
emailVerified: boolean("emailVerified").default(false).notNull(),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||
import type { User } from "../../drizzle/schema";
|
||||
import { verifyToken } from "./auth";
|
||||
import { getUserById } from "../db";
|
||||
import { getUserById, getUserByOpenId } from "../db";
|
||||
import { COOKIE_NAME } from "@shared/const";
|
||||
import { sdk } from "./sdk";
|
||||
|
||||
export type TrpcContext = {
|
||||
req: CreateExpressContextOptions["req"];
|
||||
@@ -28,12 +29,18 @@ export async function createContext(
|
||||
}
|
||||
|
||||
if (token) {
|
||||
// Vérifier et décoder le token
|
||||
const decoded = verifyToken(token);
|
||||
|
||||
if (decoded) {
|
||||
// Récupérer l'utilisateur depuis la base de données
|
||||
user = await getUserById(decoded.userId) || null;
|
||||
// Essayer d'abord l'authentification locale (JWT avec userId)
|
||||
try {
|
||||
const decoded = verifyToken(token);
|
||||
if (decoded && decoded.userId) {
|
||||
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) {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
8
todo.md
8
todo.md
@@ -290,3 +290,11 @@
|
||||
- [x] Modifier le client tRPC pour envoyer le token dans le header Authorization
|
||||
- [x] Tester la connexion dans l'environnement de preview Manus
|
||||
- [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