refactor(auth): streamline logout route and update route tree for logout integration
This commit is contained in:
+12
-16
@@ -91,22 +91,18 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
|
||||
},
|
||||
})
|
||||
|
||||
app.post(
|
||||
'/api/v1/auth/logout',
|
||||
{ onRequest: requireAuth },
|
||||
async (request, reply) => {
|
||||
const cookie = request.headers.cookie ?? ''
|
||||
const match = cookie.match(new RegExp(`${REFRESH_COOKIE}=([^;]+)`))
|
||||
if (match?.[1]) {
|
||||
revokeRefreshSession(app.db, match[1])
|
||||
}
|
||||
reply.header(
|
||||
'Set-Cookie',
|
||||
`${REFRESH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`,
|
||||
)
|
||||
return { ok: true }
|
||||
},
|
||||
)
|
||||
app.post('/api/v1/auth/logout', async (request, reply) => {
|
||||
const cookie = request.headers.cookie ?? ''
|
||||
const match = cookie.match(new RegExp(`${REFRESH_COOKIE}=([^;]+)`))
|
||||
if (match?.[1]) {
|
||||
revokeRefreshSession(app.db, match[1])
|
||||
}
|
||||
reply.header(
|
||||
'Set-Cookie',
|
||||
`${REFRESH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`,
|
||||
)
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
app.get(
|
||||
'/api/v1/auth/me',
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as AuthRouteImport } from './routes/_auth'
|
||||
import { Route as LogoutRouteImport } from './routes/logout'
|
||||
import { Route as AuthAdminRouteImport } from './routes/_auth.admin'
|
||||
import { Route as AuthAppsRouteImport } from './routes/_auth.apps'
|
||||
import { Route as AuthAdminIndexRouteImport } from './routes/_auth.admin.index'
|
||||
@@ -25,6 +26,11 @@ const AuthRoute = AuthRouteImport.update({
|
||||
id: '/_auth',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const LogoutRoute = LogoutRouteImport.update({
|
||||
id: '/logout',
|
||||
path: '/logout',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthAdminRoute = AuthAdminRouteImport.update({
|
||||
id: '/admin',
|
||||
path: '/admin',
|
||||
@@ -48,6 +54,7 @@ const AuthAdminUsersUserIdRoute = AuthAdminUsersUserIdRouteImport.update({
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/logout': typeof LogoutRoute
|
||||
'/admin': typeof AuthAdminRouteWithChildren
|
||||
'/apps': typeof AuthAppsRoute
|
||||
'/admin/': typeof AuthAdminIndexRoute
|
||||
@@ -55,6 +62,7 @@ export interface FileRoutesByFullPath {
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/logout': typeof LogoutRoute
|
||||
'/apps': typeof AuthAppsRoute
|
||||
'/admin': typeof AuthAdminIndexRoute
|
||||
'/admin/users/$userId': typeof AuthAdminUsersUserIdRoute
|
||||
@@ -63,6 +71,7 @@ export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/_auth': typeof AuthRouteWithChildren
|
||||
'/logout': typeof LogoutRoute
|
||||
'/_auth/admin': typeof AuthAdminRouteWithChildren
|
||||
'/_auth/apps': typeof AuthAppsRoute
|
||||
'/_auth/admin/': typeof AuthAdminIndexRoute
|
||||
@@ -70,13 +79,15 @@ export interface FileRoutesById {
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/admin' | '/apps' | '/admin/' | '/admin/users/$userId'
|
||||
fullPaths:
|
||||
'/' | '/logout' | '/admin' | '/apps' | '/admin/' | '/admin/users/$userId'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/apps' | '/admin' | '/admin/users/$userId'
|
||||
to: '/' | '/logout' | '/apps' | '/admin' | '/admin/users/$userId'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/_auth'
|
||||
| '/logout'
|
||||
| '/_auth/admin'
|
||||
| '/_auth/apps'
|
||||
| '/_auth/admin/'
|
||||
@@ -86,6 +97,7 @@ export interface FileRouteTypes {
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AuthRoute: typeof AuthRouteWithChildren
|
||||
LogoutRoute: typeof LogoutRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@@ -104,6 +116,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/logout': {
|
||||
id: '/logout'
|
||||
path: '/logout'
|
||||
fullPath: '/logout'
|
||||
preLoaderRoute: typeof LogoutRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_auth/admin': {
|
||||
id: '/_auth/admin'
|
||||
path: '/admin'
|
||||
@@ -164,6 +183,7 @@ const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AuthRoute: AuthRouteWithChildren,
|
||||
LogoutRoute: LogoutRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||
import { clearToken } from '@/lib/auth'
|
||||
import { logout } from '@/queries/auth'
|
||||
|
||||
/**
|
||||
* Cross-app SSO logout landing.
|
||||
* Apps (vps-tracker, CFDM, …) redirect here after clearing their local JWT
|
||||
* so the portal session (localStorage + refresh cookie) is revoked —
|
||||
* otherwise `/?return_to=…` would immediately hand a new token back.
|
||||
*/
|
||||
export const Route = createFileRoute('/logout')({
|
||||
beforeLoad: async () => {
|
||||
try {
|
||||
await logout()
|
||||
} catch {
|
||||
/* cookie/token already gone */
|
||||
}
|
||||
clearToken()
|
||||
throw redirect({ to: '/' })
|
||||
},
|
||||
component: () => null,
|
||||
})
|
||||
@@ -94,7 +94,11 @@ pnpm --filter web dev
|
||||
|
||||
## UI аккаунта
|
||||
|
||||
SidebarFooter → **NavUser** ([app-shell-1](https://reui.io/preview/base/app-shell-1)): Настройки, Тема, Выйти → portal.
|
||||
SidebarFooter → **NavUser** ([app-shell-1](https://reui.io/preview/base/app-shell-1)): Настройки, Тема, Выйти → `AUTH_PORTAL_URL/logout`.
|
||||
|
||||
## Logout (SSO)
|
||||
|
||||
Очистить `cfdm_token` → редирект на **`/logout`** портала (не на `/?return_to=…` — иначе portal сразу выдаст новый SSO-токен).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -104,3 +108,4 @@ SidebarFooter → **NavUser** ([app-shell-1](https://reui.io/preview/base/app-sh
|
||||
| 403 «Нет доступа к приложению» | у пользователя нет app `cfdm` в portal |
|
||||
| 403 «Недостаточно прав» | нет нужного `cfdm:…` permission |
|
||||
| return_to rejected | origin CFDM не в `RETURN_TO_ALLOWLIST` |
|
||||
| «Выйти» сразу возвращает в CFDM | клиент должен открывать `/logout`, не login с `return_to` |
|
||||
|
||||
@@ -108,8 +108,15 @@ pnpm --filter web dev # :5173
|
||||
| 403 на write | Только `*:read` в permissions |
|
||||
| Loop на login | `return_to` не в `RETURN_TO_ALLOWLIST` |
|
||||
| Infinite SSO / 429 | Просроченный JWT в portal localStorage; или разный `JWT_SECRET`/`ISSUER`. Portal чистит expired token; VPS блокирует повторный handoff 12с |
|
||||
| «Выйти» сразу возвращает в приложение | Старый клиент редиректил на `/?return_to=…` при живой portal-сессии. Нужен редирект на **`/logout`** (см. ниже) |
|
||||
| CORS | Portal и VPS на разных origin — fragment handoff не требует CORS для token |
|
||||
|
||||
## Logout (SSO)
|
||||
|
||||
«Выйти» в приложении: очистить локальный JWT → `AUTH_PORTAL_URL/logout` (без `return_to`).
|
||||
|
||||
Портал на `/logout`: `POST /api/v1/auth/logout` (revoke refresh cookie) → `clearToken()` → форма логина.
|
||||
|
||||
## Production
|
||||
|
||||
- Один `JWT_SECRET` в secret store обоих сервисов
|
||||
|
||||
Reference in New Issue
Block a user