First Commit
Build and Push Auth Portal Docker Image / build-and-push (push) Successful in 5m26s
Build and Push Auth Portal Docker Image / create-release (push) Skipped

This commit is contained in:
Denozordec
2026-07-18 13:27:37 +07:00
commit bac95bdb2e
154 changed files with 26610 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@authportal/shared",
"version": "0.0.0",
"private": true,
"type": "module",
"files": ["dist", "package.json"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts",
"dev": "tsup src/index.ts --format esm --dts --watch",
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"zod": "^4.4.3"
},
"devDependencies": {
"tsup": "^8.5.0",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
}
}
+300
View File
@@ -0,0 +1,300 @@
import { z } from 'zod'
export const APP_IDS = ['cfdm', 'vps', 'bgp'] as const
export type AppId = (typeof APP_IDS)[number]
export const appIdSchema = z.enum(APP_IDS)
export const PERMISSION_ACTIONS = ['read', 'write', 'admin'] as const
export type PermissionAction = (typeof PERMISSION_ACTIONS)[number]
export const permissionActionSchema = z.enum(PERMISSION_ACTIONS)
export type AppMeta = {
id: AppId
title: string
description: string
url: string
}
export const APPS: AppMeta[] = [
{
id: 'cfdm',
title: 'Cloudflare Domain Manager',
description: 'Домены, DNS, сертификаты, группы и сервисы',
url: 'https://cfdm.shnt.top',
},
{
id: 'vps',
title: 'VPS Tracker',
description: 'Серверы, аккаунты, платежи и синхронизация',
url: 'https://vps.shnt.top',
},
{
id: 'bgp',
title: 'EvoBGP',
description: 'Модули, peers, сеть и apply',
url: 'https://bgp.shnt.top',
},
]
export type CatalogSection = {
id: string
title: string
description: string
actions: PermissionAction[]
}
export type AppPermissionCatalog = {
appId: AppId
title: string
sections: CatalogSection[]
}
function section(
id: string,
title: string,
description: string,
actions: PermissionAction[] = ['read', 'write'],
): CatalogSection {
return { id, title, description, actions }
}
export const PERMISSION_CATALOG: AppPermissionCatalog[] = [
{
appId: 'cfdm',
title: 'Cloudflare Domain Manager',
sections: [
section('domains', 'Домены', 'Список и карточки доменов'),
section('dns', 'DNS', 'DNS-записи'),
section('certificates', 'Сертификаты', 'TLS-сертификаты'),
section('groups', 'Группы', 'Группы доменов'),
section('services', 'Сервисы', 'Сервисы и привязки'),
section('settings', 'Настройки', 'Настройки приложения', ['admin']),
],
},
{
appId: 'vps',
title: 'VPS Tracker',
sections: [
section('dashboard', 'Дашборд', 'Обзор и KPI', ['read']),
section('vps', 'VPS', 'Виртуальные серверы'),
section('accounts', 'Аккаунты', 'Аккаунты провайдеров'),
section('payments', 'Платежи', 'Платежи и баланс'),
section('sync', 'Синхронизация', 'Запуск sync', ['write']),
section('settings', 'Настройки', 'Настройки приложения', ['admin']),
],
},
{
appId: 'bgp',
title: 'EvoBGP',
sections: [
section('modules', 'Модули', 'Модули префиксов'),
section('peers', 'Peers', 'BGP peers'),
section('network', 'Сеть', 'Спикеры и сеть'),
section('apply', 'Apply', 'Apply / rollback', ['write']),
section('access', 'Доступ', 'API-ключи', ['admin']),
section('settings', 'Настройки', 'Настройки tenant', ['admin']),
],
},
]
export function permissionKey(
appId: AppId,
sectionId: string,
action: PermissionAction,
): string {
return `${appId}:${sectionId}:${action}`
}
/** Hierarchy: admin ⊃ write ⊃ read within the same section. */
export function hasPermission(
granted: readonly string[],
required: string,
): boolean {
if (granted.includes(required)) return true
const parts = required.split(':')
if (parts.length !== 3) return false
const [app, section, action] = parts
if (action === 'read') {
return (
granted.includes(`${app}:${section}:write`) ||
granted.includes(`${app}:${section}:admin`)
)
}
if (action === 'write') {
return granted.includes(`${app}:${section}:admin`)
}
return false
}
/**
* Allowlist entries: `.shnt.top`, `localhost`, or full origins `http://localhost:5173`.
*/
export function isReturnToAllowed(
returnTo: string,
allowlistCsv: string,
): boolean {
let url: URL
try {
url = new URL(returnTo)
} catch {
return false
}
if (url.protocol !== 'http:' && url.protocol !== 'https:') return false
const entries = allowlistCsv
.split(',')
.map((s) => s.trim())
.filter(Boolean)
for (const entry of entries) {
if (entry.startsWith('.')) {
const suffix = entry.slice(1)
if (url.hostname === suffix || url.hostname.endsWith(entry)) return true
continue
}
if (entry.includes('://')) {
try {
if (url.origin === new URL(entry).origin) return true
} catch {
/* ignore */
}
continue
}
if (url.hostname === entry || url.hostname.endsWith(`.${entry}`)) {
return true
}
}
return false
}
export function buildSsoRedirectUrl(
returnTo: string,
accessToken: string,
expiresAt: string,
): string {
const hash = new URLSearchParams({
access_token: accessToken,
expires_at: expiresAt,
})
const base = returnTo.split('#')[0] ?? returnTo
return `${base}#${hash.toString()}`
}
export function allPermissionKeys(): string[] {
const keys: string[] = []
for (const app of PERMISSION_CATALOG) {
for (const sec of app.sections) {
for (const action of sec.actions) {
keys.push(permissionKey(app.appId, sec.id, action))
}
}
}
return keys
}
export const permissionKeySchema = z
.string()
.regex(/^[a-z]+:[a-z0-9_]+:(read|write|admin)$/)
export const meResponseSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
is_admin: z.boolean(),
apps: z.array(appIdSchema),
permissions: z.array(z.string()),
})
export type MeResponse = z.infer<typeof meResponseSchema>
export const loginRequestSchema = z.object({
email: z.string().email(),
password: z.string().min(1),
return_to: z.string().url().optional(),
})
export type LoginRequest = z.infer<typeof loginRequestSchema>
export const loginResponseSchema = z.object({
access_token: z.string(),
expires_at: z.string(),
token_type: z.literal('Bearer'),
user: meResponseSchema,
})
export type LoginResponse = z.infer<typeof loginResponseSchema>
export const accessTokenClaimsSchema = z.object({
sub: z.string(),
email: z.string().email(),
name: z.string(),
apps: z.array(z.string()),
permissions: z.array(z.string()),
is_admin: z.boolean().optional(),
iss: z.string(),
aud: z.string().optional(),
exp: z.number(),
iat: z.number(),
})
export type AccessTokenClaims = z.infer<typeof accessTokenClaimsSchema>
export const catalogResponseSchema = z.object({
apps: z.array(
z.object({
id: appIdSchema,
title: z.string(),
description: z.string(),
url: z.string(),
}),
),
permissions: z.array(
z.object({
appId: appIdSchema,
title: z.string(),
sections: z.array(
z.object({
id: z.string(),
title: z.string(),
description: z.string(),
actions: z.array(permissionActionSchema),
}),
),
}),
),
})
export type CatalogResponse = z.infer<typeof catalogResponseSchema>
export const adminUserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
is_admin: z.boolean(),
disabled: z.boolean(),
apps: z.array(appIdSchema),
permissions: z.array(z.string()),
created_at: z.string(),
updated_at: z.string(),
})
export type AdminUser = z.infer<typeof adminUserSchema>
export const createUserRequestSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
password: z.string().min(6),
is_admin: z.boolean().default(false),
apps: z.array(appIdSchema).default([]),
permissions: z.array(permissionKeySchema).default([]),
})
export type CreateUserRequest = z.infer<typeof createUserRequestSchema>
export const patchUserRequestSchema = z.object({
email: z.string().email().optional(),
name: z.string().min(1).optional(),
password: z.string().min(6).optional(),
is_admin: z.boolean().optional(),
disabled: z.boolean().optional(),
})
export type PatchUserRequest = z.infer<typeof patchUserRequestSchema>
export const putUserAccessRequestSchema = z.object({
apps: z.array(appIdSchema),
permissions: z.array(permissionKeySchema),
})
export type PutUserAccessRequest = z.infer<typeof putUserAccessRequestSchema>
+1
View File
@@ -0,0 +1 @@
export * from './contracts/auth.js'
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}