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(
|
app.post('/api/v1/auth/logout', async (request, reply) => {
|
||||||
'/api/v1/auth/logout',
|
const cookie = request.headers.cookie ?? ''
|
||||||
{ onRequest: requireAuth },
|
const match = cookie.match(new RegExp(`${REFRESH_COOKIE}=([^;]+)`))
|
||||||
async (request, reply) => {
|
if (match?.[1]) {
|
||||||
const cookie = request.headers.cookie ?? ''
|
revokeRefreshSession(app.db, match[1])
|
||||||
const match = cookie.match(new RegExp(`${REFRESH_COOKIE}=([^;]+)`))
|
}
|
||||||
if (match?.[1]) {
|
reply.header(
|
||||||
revokeRefreshSession(app.db, match[1])
|
'Set-Cookie',
|
||||||
}
|
`${REFRESH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`,
|
||||||
reply.header(
|
)
|
||||||
'Set-Cookie',
|
return { ok: true }
|
||||||
`${REFRESH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`,
|
})
|
||||||
)
|
|
||||||
return { ok: true }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
'/api/v1/auth/me',
|
'/api/v1/auth/me',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as AuthRouteImport } from './routes/_auth'
|
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 AuthAdminRouteImport } from './routes/_auth.admin'
|
||||||
import { Route as AuthAppsRouteImport } from './routes/_auth.apps'
|
import { Route as AuthAppsRouteImport } from './routes/_auth.apps'
|
||||||
import { Route as AuthAdminIndexRouteImport } from './routes/_auth.admin.index'
|
import { Route as AuthAdminIndexRouteImport } from './routes/_auth.admin.index'
|
||||||
@@ -25,6 +26,11 @@ const AuthRoute = AuthRouteImport.update({
|
|||||||
id: '/_auth',
|
id: '/_auth',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const LogoutRoute = LogoutRouteImport.update({
|
||||||
|
id: '/logout',
|
||||||
|
path: '/logout',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const AuthAdminRoute = AuthAdminRouteImport.update({
|
const AuthAdminRoute = AuthAdminRouteImport.update({
|
||||||
id: '/admin',
|
id: '/admin',
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
@@ -48,6 +54,7 @@ const AuthAdminUsersUserIdRoute = AuthAdminUsersUserIdRouteImport.update({
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/logout': typeof LogoutRoute
|
||||||
'/admin': typeof AuthAdminRouteWithChildren
|
'/admin': typeof AuthAdminRouteWithChildren
|
||||||
'/apps': typeof AuthAppsRoute
|
'/apps': typeof AuthAppsRoute
|
||||||
'/admin/': typeof AuthAdminIndexRoute
|
'/admin/': typeof AuthAdminIndexRoute
|
||||||
@@ -55,6 +62,7 @@ export interface FileRoutesByFullPath {
|
|||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/logout': typeof LogoutRoute
|
||||||
'/apps': typeof AuthAppsRoute
|
'/apps': typeof AuthAppsRoute
|
||||||
'/admin': typeof AuthAdminIndexRoute
|
'/admin': typeof AuthAdminIndexRoute
|
||||||
'/admin/users/$userId': typeof AuthAdminUsersUserIdRoute
|
'/admin/users/$userId': typeof AuthAdminUsersUserIdRoute
|
||||||
@@ -63,6 +71,7 @@ export interface FileRoutesById {
|
|||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/_auth': typeof AuthRouteWithChildren
|
'/_auth': typeof AuthRouteWithChildren
|
||||||
|
'/logout': typeof LogoutRoute
|
||||||
'/_auth/admin': typeof AuthAdminRouteWithChildren
|
'/_auth/admin': typeof AuthAdminRouteWithChildren
|
||||||
'/_auth/apps': typeof AuthAppsRoute
|
'/_auth/apps': typeof AuthAppsRoute
|
||||||
'/_auth/admin/': typeof AuthAdminIndexRoute
|
'/_auth/admin/': typeof AuthAdminIndexRoute
|
||||||
@@ -70,13 +79,15 @@ export interface FileRoutesById {
|
|||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/admin' | '/apps' | '/admin/' | '/admin/users/$userId'
|
fullPaths:
|
||||||
|
'/' | '/logout' | '/admin' | '/apps' | '/admin/' | '/admin/users/$userId'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/apps' | '/admin' | '/admin/users/$userId'
|
to: '/' | '/logout' | '/apps' | '/admin' | '/admin/users/$userId'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/_auth'
|
| '/_auth'
|
||||||
|
| '/logout'
|
||||||
| '/_auth/admin'
|
| '/_auth/admin'
|
||||||
| '/_auth/apps'
|
| '/_auth/apps'
|
||||||
| '/_auth/admin/'
|
| '/_auth/admin/'
|
||||||
@@ -86,6 +97,7 @@ export interface FileRouteTypes {
|
|||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
AuthRoute: typeof AuthRouteWithChildren
|
AuthRoute: typeof AuthRouteWithChildren
|
||||||
|
LogoutRoute: typeof LogoutRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -104,6 +116,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthRouteImport
|
preLoaderRoute: typeof AuthRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/logout': {
|
||||||
|
id: '/logout'
|
||||||
|
path: '/logout'
|
||||||
|
fullPath: '/logout'
|
||||||
|
preLoaderRoute: typeof LogoutRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/_auth/admin': {
|
'/_auth/admin': {
|
||||||
id: '/_auth/admin'
|
id: '/_auth/admin'
|
||||||
path: '/admin'
|
path: '/admin'
|
||||||
@@ -164,6 +183,7 @@ const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
|||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
AuthRoute: AuthRouteWithChildren,
|
AuthRoute: AuthRouteWithChildren,
|
||||||
|
LogoutRoute: LogoutRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._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 аккаунта
|
## 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
|
## Troubleshooting
|
||||||
|
|
||||||
@@ -104,3 +108,4 @@ SidebarFooter → **NavUser** ([app-shell-1](https://reui.io/preview/base/app-sh
|
|||||||
| 403 «Нет доступа к приложению» | у пользователя нет app `cfdm` в portal |
|
| 403 «Нет доступа к приложению» | у пользователя нет app `cfdm` в portal |
|
||||||
| 403 «Недостаточно прав» | нет нужного `cfdm:…` permission |
|
| 403 «Недостаточно прав» | нет нужного `cfdm:…` permission |
|
||||||
| return_to rejected | origin CFDM не в `RETURN_TO_ALLOWLIST` |
|
| 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 |
|
| 403 на write | Только `*:read` в permissions |
|
||||||
| Loop на login | `return_to` не в `RETURN_TO_ALLOWLIST` |
|
| Loop на login | `return_to` не в `RETURN_TO_ALLOWLIST` |
|
||||||
| Infinite SSO / 429 | Просроченный JWT в portal localStorage; или разный `JWT_SECRET`/`ISSUER`. Portal чистит expired token; VPS блокирует повторный handoff 12с |
|
| 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 |
|
| 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
|
## Production
|
||||||
|
|
||||||
- Один `JWT_SECRET` в secret store обоих сервисов
|
- Один `JWT_SECRET` в secret store обоих сервисов
|
||||||
|
|||||||
Reference in New Issue
Block a user