refactor(auth): streamline logout route and update route tree for logout integration
Build and Push Auth Portal Docker Image / build-and-push (push) Successful in 3m15s
Build and Push Auth Portal Docker Image / create-release (push) Skipped

This commit is contained in:
Denozordec
2026-07-18 18:36:21 +07:00
parent 2a6adf7af8
commit a4ece3d344
5 changed files with 69 additions and 19 deletions
+12 -16
View File
@@ -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',
+22 -2
View File
@@ -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)
+22
View File
@@ -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,
})
+6 -1
View File
@@ -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` |
+7
View File
@@ -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 обоих сервисов