Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/cms/src/api/language-geo/routes/language-geo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ export default {
path: "/language-geo",
handler: "language-geo.index",
config: {
auth: false,
policies: [],
middlewares: [],
middlewares: ["global::api-token-auth"],
},
},
],
Expand Down
3 changes: 2 additions & 1 deletion apps/cms/src/api/video-coverage/routes/video-coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ export default {
path: "/video-coverage",
handler: "video-coverage.index",
config: {
auth: false,
policies: [],
middlewares: [],
middlewares: ["global::api-token-auth"],
},
},
],
Expand Down
73 changes: 73 additions & 0 deletions apps/cms/src/middlewares/api-token-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { Core } from "@strapi/strapi"

/**
* Middleware that validates Bearer tokens against Strapi's API token store.
*
* Strapi v5 custom routes with `auth: false` bypass all built-in auth.
* Removing `auth: false` enables the Users & Permissions plugin, which
* requires admin-configured role permissions — not API token auth.
*
* This middleware bridges the gap: keep `auth: false` to bypass U&P,
* but validate the Bearer token against Strapi's hashed API token table.
*
* Usage in route config:
* config: {
* auth: false,
* middlewares: ["global::api-token-auth"],
* }
*/
export default (_config: unknown, { strapi }: { strapi: Core.Strapi }) => {
return async (
ctx: {
request: { headers: Record<string, string | undefined> }
status: number
body: unknown
},
next: () => Promise<void>,
) => {
const authorization = ctx.request.headers["authorization"]

if (!authorization?.startsWith("Bearer ")) {
ctx.status = 401
ctx.body = { error: "Missing or invalid Authorization header" }
return
}

const token = authorization.slice(7)

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Strapi admin services are not in public types
const apiTokenService = (strapi.admin as any)?.services?.["api-token"] as
| {
hash?: (accessKey: string) => string
getBy?: (filter: { accessKey: string }) => Promise<{
id: number
type: string
expiresAt: string | null
} | null>
}
| undefined

if (!apiTokenService?.hash || !apiTokenService?.getBy) {
ctx.status = 503
ctx.body = { error: "API token service not available" }
return
}

const hashedToken = apiTokenService.hash(token)
const apiToken = await apiTokenService.getBy({ accessKey: hashedToken })

if (!apiToken) {
ctx.status = 401
ctx.body = { error: "Invalid API token" }
return
}

if (apiToken.expiresAt && new Date(apiToken.expiresAt) < new Date()) {
ctx.status = 401
ctx.body = { error: "API token expired" }
return
}

await next()
}
}
Loading