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(
'/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',
+22 -2
View File
@@ -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)
+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 аккаунта
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` |
+7
View File
@@ -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 обоих сервисов