feat(auth): expose current license app permissions
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -92,11 +92,26 @@ model License {
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
userLicenses UserLicense[]
|
||||
appPermissions LicenseAppPermission[]
|
||||
|
||||
@@index([isActive])
|
||||
@@map("licenses")
|
||||
}
|
||||
|
||||
model LicenseAppPermission {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
licenseId String @map("license_id") @db.Uuid
|
||||
appKey String @map("app_key")
|
||||
canLaunch Boolean @default(true) @map("can_launch")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
license License @relation(fields: [licenseId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([licenseId, appKey])
|
||||
@@index([appKey])
|
||||
@@map("license_app_permissions")
|
||||
}
|
||||
|
||||
model UserLicense {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it, mock } from 'bun:test'
|
||||
|
||||
const findFirst = mock(async (): Promise<unknown> => null)
|
||||
mock.module('../prisma', () => ({ prisma: { userLicense: { findFirst } } }))
|
||||
const { getLauncherAppPolicy } = await import('./license-policy')
|
||||
|
||||
describe('launcher license policy', () => {
|
||||
it('returns the current license app policy', async () => {
|
||||
findFirst.mockResolvedValueOnce({ license: { appPermissions: [{ appKey: 'LMS', canLaunch: true }, { appKey: 'OTHER', canLaunch: false }] } })
|
||||
expect(await getLauncherAppPolicy('student')).toEqual({
|
||||
defaultCanLaunch: true,
|
||||
permissions: [{ appKey: 'LMS', canLaunch: true }, { appKey: 'OTHER', canLaunch: false }],
|
||||
})
|
||||
expect(findFirst).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
userId: 'student', startsAt: { lte: expect.any(Date) },
|
||||
OR: [{ expiresAt: null }, { expiresAt: { gt: expect.any(Date) } }],
|
||||
license: { isActive: true },
|
||||
}),
|
||||
orderBy: { startsAt: 'desc' },
|
||||
}))
|
||||
})
|
||||
|
||||
it('denies by default without an active license', async () => {
|
||||
findFirst.mockResolvedValueOnce(null)
|
||||
expect(await getLauncherAppPolicy('student')).toEqual({ defaultCanLaunch: false, permissions: [] })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { prisma } from '../prisma'
|
||||
|
||||
export async function getLauncherAppPolicy(userId: string) {
|
||||
const now = new Date()
|
||||
const current = await prisma.userLicense.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
startsAt: { lte: now },
|
||||
OR: [{ expiresAt: null }, { expiresAt: { gt: now } }],
|
||||
license: { isActive: true }
|
||||
},
|
||||
orderBy: { startsAt: 'desc' },
|
||||
select: {
|
||||
license: { select: { appPermissions: { select: { appKey: true, canLaunch: true } } } }
|
||||
}
|
||||
})
|
||||
return {
|
||||
// Match the admin license policy: unspecified apps are allowed for active licenses.
|
||||
defaultCanLaunch: Boolean(current),
|
||||
permissions: current?.license.appPermissions ?? []
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { verifyPassword } from '../security/password'
|
||||
import { signAccessToken, verifyAccessToken } from '../security/tokens'
|
||||
import type { AuthUser } from '../types'
|
||||
import { authRequired, type AuthVariables } from './middleware'
|
||||
import { getLauncherAppPolicy } from './license-policy'
|
||||
import {
|
||||
clearLoginAttempts,
|
||||
assignUserAffiliation,
|
||||
@@ -686,6 +687,10 @@ authRoutes.post('/logout', async (c) => {
|
||||
return c.json({ ok: true })
|
||||
})
|
||||
|
||||
authRoutes.get('/launcher-app-policy', authRequired, async (c) => {
|
||||
return c.json(await getLauncherAppPolicy(c.get('user').id))
|
||||
})
|
||||
|
||||
authRoutes.get('/me', authRequired, (c) => {
|
||||
return c.json({ user: userResponse(c.get('user')) })
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user