feat(api, web): integrate pod routes and user hierarchy management
- Added pod routes to the API and registered them in the app. - Implemented user hierarchy management with new database tables for user relationships and pod settings. - Enhanced user detail and grid components to support pod settings, including the ability to create child users and manage their limits. - Updated authentication guards to include pod-specific authorization checks. - Improved user interface for managing pod access and settings in the user detail sheet and grid view. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -13,6 +13,7 @@ import type { AppConfig } from './config.js'
|
||||
import { TelemtClient } from './services/telemt-client.js'
|
||||
import { authRoutes, ensureBootstrapAdmin } from './routes/auth.js'
|
||||
import { telemtRoutes, fleetRoutes, agentProtocolRoutes } from './routes/telemt.js'
|
||||
import { podRoutes } from './routes/pod.js'
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
@@ -82,6 +83,7 @@ export async function buildApp(opts: {
|
||||
await app.register(telemtRoutes)
|
||||
await app.register(fleetRoutes)
|
||||
await app.register(agentProtocolRoutes)
|
||||
await app.register(podRoutes)
|
||||
|
||||
app.get('/install-agent.sh', async (_request, reply) => {
|
||||
const candidates = [
|
||||
|
||||
@@ -8,6 +8,11 @@ export interface AuthOperator {
|
||||
role: string
|
||||
}
|
||||
|
||||
export interface PodParent {
|
||||
username: string
|
||||
role: 'pod_parent'
|
||||
}
|
||||
|
||||
declare module '@fastify/jwt' {
|
||||
interface FastifyJWT {
|
||||
payload: { sub: string; username: string; role: string }
|
||||
@@ -22,6 +27,12 @@ export async function requireAuth(request: FastifyRequest, reply: FastifyReply)
|
||||
return reply.code(401).send({ error: { code: 'unauthorized', message: 'Требуется вход' } })
|
||||
}
|
||||
|
||||
if (request.user.role === 'pod_parent') {
|
||||
return reply.code(401).send({
|
||||
error: { code: 'unauthorized', message: 'Требуется вход оператора' },
|
||||
})
|
||||
}
|
||||
|
||||
const row = request.server.db
|
||||
.select()
|
||||
.from(operators)
|
||||
@@ -42,3 +53,26 @@ export async function requireAuth(request: FastifyRequest, reply: FastifyReply)
|
||||
export function getOperator(request: FastifyRequest): AuthOperator {
|
||||
return (request as FastifyRequest & { operator: AuthOperator }).operator
|
||||
}
|
||||
|
||||
export async function requirePodAuth(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify()
|
||||
} catch {
|
||||
return reply.code(401).send({ error: { code: 'unauthorized', message: 'Требуется вход' } })
|
||||
}
|
||||
|
||||
if (request.user.role !== 'pod_parent' || !request.user.username) {
|
||||
return reply.code(401).send({
|
||||
error: { code: 'unauthorized', message: 'Требуется сессия /pod' },
|
||||
})
|
||||
}
|
||||
|
||||
;(request as FastifyRequest & { podParent: PodParent }).podParent = {
|
||||
username: request.user.username,
|
||||
role: 'pod_parent',
|
||||
}
|
||||
}
|
||||
|
||||
export function getPodParent(request: FastifyRequest): PodParent {
|
||||
return (request as FastifyRequest & { podParent: PodParent }).podParent
|
||||
}
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import { count, eq } from 'drizzle-orm'
|
||||
import { z } from 'zod'
|
||||
import { userHierarchy, userPodSettings } from '@telemt/db'
|
||||
import { getOperator, getPodParent, requireAuth, requirePodAuth } from '../plugins/auth-guards.js'
|
||||
import {
|
||||
fetchTelemtUsers,
|
||||
resolveUserBySecret,
|
||||
type TelemtUserInfo,
|
||||
} from '../services/pod-auth.js'
|
||||
|
||||
const sessionBodySchema = z.object({
|
||||
secretOrLink: z.string().min(1),
|
||||
})
|
||||
|
||||
const createChildBodySchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(64)
|
||||
.regex(/^[A-Za-z0-9_.-]+$/),
|
||||
secret: z
|
||||
.string()
|
||||
.regex(/^[0-9a-fA-F]{32}$/)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const podSettingsBodySchema = z.object({
|
||||
canCreateChildren: z.boolean(),
|
||||
maxChildren: z.number().int().min(0).max(10_000),
|
||||
})
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString()
|
||||
}
|
||||
|
||||
function defaultPodSettings(username: string) {
|
||||
return {
|
||||
username,
|
||||
canCreateChildren: false,
|
||||
maxChildren: 0,
|
||||
updatedAt: nowIso(),
|
||||
}
|
||||
}
|
||||
|
||||
function getPodSettingsRow(
|
||||
app: FastifyInstance,
|
||||
username: string,
|
||||
): {
|
||||
username: string
|
||||
canCreateChildren: boolean
|
||||
maxChildren: number
|
||||
updatedAt: string
|
||||
} {
|
||||
const row = app.db
|
||||
.select()
|
||||
.from(userPodSettings)
|
||||
.where(eq(userPodSettings.username, username))
|
||||
.get()
|
||||
if (!row) return defaultPodSettings(username)
|
||||
return {
|
||||
username: row.username,
|
||||
canCreateChildren: Boolean(row.canCreateChildren),
|
||||
maxChildren: row.maxChildren,
|
||||
updatedAt: row.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
function countChildren(app: FastifyInstance, parentUsername: string): number {
|
||||
const row = app.db
|
||||
.select({ value: count() })
|
||||
.from(userHierarchy)
|
||||
.where(eq(userHierarchy.parentUsername, parentUsername))
|
||||
.get()
|
||||
return Number(row?.value ?? 0)
|
||||
}
|
||||
|
||||
function isChildUser(app: FastifyInstance, username: string): boolean {
|
||||
const row = app.db
|
||||
.select()
|
||||
.from(userHierarchy)
|
||||
.where(eq(userHierarchy.childUsername, username))
|
||||
.get()
|
||||
return Boolean(row)
|
||||
}
|
||||
|
||||
function remainingSlots(
|
||||
canCreateChildren: boolean,
|
||||
maxChildren: number,
|
||||
childrenCount: number,
|
||||
): number {
|
||||
if (!canCreateChildren) return 0
|
||||
return Math.max(0, maxChildren - childrenCount)
|
||||
}
|
||||
|
||||
function assertStandaloneTelemt(app: FastifyInstance): boolean {
|
||||
return Boolean(app.config.telemtApiUrl)
|
||||
}
|
||||
|
||||
export async function podRoutes(app: FastifyInstance) {
|
||||
app.post(
|
||||
'/api/pod/session',
|
||||
{
|
||||
config: {
|
||||
rateLimit: { max: 20, timeWindow: '1 minute' },
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
if (!assertStandaloneTelemt(app)) {
|
||||
return reply.code(503).send({
|
||||
error: { code: 'telemt_unavailable', message: 'Telemt API не настроен' },
|
||||
})
|
||||
}
|
||||
|
||||
const parsed = sessionBodySchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.code(400).send({
|
||||
error: { code: 'validation_error', message: 'Укажите secret или ссылку tg://proxy' },
|
||||
})
|
||||
}
|
||||
|
||||
let user: TelemtUserInfo | null
|
||||
try {
|
||||
user = await resolveUserBySecret(app.telemt, parsed.data.secretOrLink)
|
||||
} catch {
|
||||
return reply.code(502).send({
|
||||
error: { code: 'telemt_error', message: 'Не удалось связаться с Telemt' },
|
||||
})
|
||||
}
|
||||
|
||||
if (!user?.username) {
|
||||
return reply.code(401).send({
|
||||
error: { code: 'invalid_secret', message: 'Пользователь с таким секретом не найден' },
|
||||
})
|
||||
}
|
||||
|
||||
if (isChildUser(app, user.username)) {
|
||||
return reply.code(403).send({
|
||||
error: {
|
||||
code: 'pod_child_forbidden',
|
||||
message: 'Подчинённый пользователь не может входить в /pod',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const settings = getPodSettingsRow(app, user.username)
|
||||
if (!settings.canCreateChildren) {
|
||||
return reply.code(403).send({
|
||||
error: {
|
||||
code: 'pod_create_disabled',
|
||||
message: 'Создание подчинённых для этого аккаунта не разрешено',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const accessToken = await reply.jwtSign(
|
||||
{
|
||||
sub: `pod:${user.username}`,
|
||||
username: user.username,
|
||||
role: 'pod_parent',
|
||||
},
|
||||
{ expiresIn: `${app.config.jwtTtlHours}h` },
|
||||
)
|
||||
|
||||
const childrenCount = countChildren(app, user.username)
|
||||
return {
|
||||
accessToken,
|
||||
username: user.username,
|
||||
canCreateChildren: settings.canCreateChildren,
|
||||
maxChildren: settings.maxChildren,
|
||||
childrenCount,
|
||||
remaining: remainingSlots(
|
||||
settings.canCreateChildren,
|
||||
settings.maxChildren,
|
||||
childrenCount,
|
||||
),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
app.get(
|
||||
'/api/pod/me',
|
||||
{ preHandler: requirePodAuth },
|
||||
async (request, reply) => {
|
||||
const parent = getPodParent(request)
|
||||
if (isChildUser(app, parent.username)) {
|
||||
return reply.code(403).send({
|
||||
error: {
|
||||
code: 'pod_child_forbidden',
|
||||
message: 'Подчинённый пользователь не может использовать /pod',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const settings = getPodSettingsRow(app, parent.username)
|
||||
const childrenCount = countChildren(app, parent.username)
|
||||
return {
|
||||
username: parent.username,
|
||||
canCreateChildren: settings.canCreateChildren,
|
||||
maxChildren: settings.maxChildren,
|
||||
childrenCount,
|
||||
remaining: remainingSlots(
|
||||
settings.canCreateChildren,
|
||||
settings.maxChildren,
|
||||
childrenCount,
|
||||
),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
app.get(
|
||||
'/api/pod/children',
|
||||
{ preHandler: requirePodAuth },
|
||||
async (request) => {
|
||||
const parent = getPodParent(request)
|
||||
const links = app.db
|
||||
.select()
|
||||
.from(userHierarchy)
|
||||
.where(eq(userHierarchy.parentUsername, parent.username))
|
||||
.all()
|
||||
|
||||
const telemtUsers = await fetchTelemtUsers(app.telemt)
|
||||
const byName = new Map(telemtUsers.map((u) => [u.username, u]))
|
||||
|
||||
const children = links.map((row) => {
|
||||
const user = byName.get(row.childUsername) ?? null
|
||||
return {
|
||||
username: row.childUsername,
|
||||
parentUsername: row.parentUsername,
|
||||
createdAt: row.createdAt,
|
||||
user,
|
||||
}
|
||||
})
|
||||
|
||||
return { children }
|
||||
},
|
||||
)
|
||||
|
||||
app.post(
|
||||
'/api/pod/children',
|
||||
{ preHandler: requirePodAuth },
|
||||
async (request, reply) => {
|
||||
const parent = getPodParent(request)
|
||||
|
||||
if (isChildUser(app, parent.username)) {
|
||||
return reply.code(403).send({
|
||||
error: {
|
||||
code: 'pod_child_forbidden',
|
||||
message: 'Подчинённый не может создавать пользователей',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const settings = getPodSettingsRow(app, parent.username)
|
||||
if (!settings.canCreateChildren) {
|
||||
return reply.code(403).send({
|
||||
error: {
|
||||
code: 'pod_create_disabled',
|
||||
message: 'Создание подчинённых не разрешено',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const childrenCount = countChildren(app, parent.username)
|
||||
if (childrenCount >= settings.maxChildren) {
|
||||
return reply.code(403).send({
|
||||
error: {
|
||||
code: 'pod_limit_reached',
|
||||
message: `Достигнут лимит подчинённых (${settings.maxChildren})`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const parsed = createChildBodySchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.code(400).send({
|
||||
error: {
|
||||
code: 'validation_error',
|
||||
message: 'Некорректные данные: username (и опционально secret 32 hex)',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const childUsername = parsed.data.username
|
||||
if (isChildUser(app, childUsername) || childUsername === parent.username) {
|
||||
return reply.code(409).send({
|
||||
error: {
|
||||
code: 'conflict',
|
||||
message: 'Пользователь уже существует в иерархии или совпадает с родителем',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const { status, envelope } = await app.telemt.request({
|
||||
method: 'POST',
|
||||
path: '/v1/users',
|
||||
body: {
|
||||
username: childUsername,
|
||||
...(parsed.data.secret ? { secret: parsed.data.secret } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
if (status >= 400 || !envelope.ok) {
|
||||
return reply.code(status >= 400 ? status : 502).send({
|
||||
error: envelope.error ?? {
|
||||
code: 'telemt_error',
|
||||
message: 'Не удалось создать пользователя в Telemt',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const createdAt = nowIso()
|
||||
try {
|
||||
app.db
|
||||
.insert(userHierarchy)
|
||||
.values({
|
||||
childUsername,
|
||||
parentUsername: parent.username,
|
||||
createdAt,
|
||||
})
|
||||
.run()
|
||||
} catch {
|
||||
return reply.code(409).send({
|
||||
error: {
|
||||
code: 'hierarchy_conflict',
|
||||
message: 'Пользователь создан в Telemt, но уже есть в иерархии',
|
||||
},
|
||||
data: envelope.data,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
data: envelope.data,
|
||||
hierarchy: {
|
||||
childUsername,
|
||||
parentUsername: parent.username,
|
||||
createdAt,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
app.post('/api/pod/logout', { preHandler: requirePodAuth }, async () => {
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
app.get(
|
||||
'/api/user-hierarchy',
|
||||
{ preHandler: requireAuth },
|
||||
async () => {
|
||||
const links = app.db.select().from(userHierarchy).all()
|
||||
const settingsRows = app.db.select().from(userPodSettings).all()
|
||||
|
||||
const childrenByParent: Record<string, string[]> = {}
|
||||
const parentByChild: Record<string, string> = {}
|
||||
for (const row of links) {
|
||||
parentByChild[row.childUsername] = row.parentUsername
|
||||
const list = childrenByParent[row.parentUsername] ?? []
|
||||
list.push(row.childUsername)
|
||||
childrenByParent[row.parentUsername] = list
|
||||
}
|
||||
|
||||
const settings: Record<
|
||||
string,
|
||||
{ canCreateChildren: boolean; maxChildren: number; childrenCount: number }
|
||||
> = {}
|
||||
for (const row of settingsRows) {
|
||||
settings[row.username] = {
|
||||
canCreateChildren: Boolean(row.canCreateChildren),
|
||||
maxChildren: row.maxChildren,
|
||||
childrenCount: childrenByParent[row.username]?.length ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Include parents that have children but no settings row yet
|
||||
for (const parent of Object.keys(childrenByParent)) {
|
||||
if (!settings[parent]) {
|
||||
settings[parent] = {
|
||||
canCreateChildren: false,
|
||||
maxChildren: 0,
|
||||
childrenCount: childrenByParent[parent]?.length ?? 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { childrenByParent, parentByChild, settings }
|
||||
},
|
||||
)
|
||||
|
||||
app.get(
|
||||
'/api/user-pod-settings',
|
||||
{ preHandler: requireAuth },
|
||||
async () => {
|
||||
const rows = app.db.select().from(userPodSettings).all()
|
||||
return {
|
||||
settings: rows.map((row) => ({
|
||||
username: row.username,
|
||||
canCreateChildren: Boolean(row.canCreateChildren),
|
||||
maxChildren: row.maxChildren,
|
||||
childrenCount: countChildren(app, row.username),
|
||||
updatedAt: row.updatedAt,
|
||||
})),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
app.put(
|
||||
'/api/user-pod-settings/:username',
|
||||
{ preHandler: requireAuth },
|
||||
async (request, reply) => {
|
||||
getOperator(request)
|
||||
const username = (request.params as { username: string }).username
|
||||
if (!username) {
|
||||
return reply.code(400).send({
|
||||
error: { code: 'validation_error', message: 'username обязателен' },
|
||||
})
|
||||
}
|
||||
|
||||
if (isChildUser(app, username)) {
|
||||
return reply.code(400).send({
|
||||
error: {
|
||||
code: 'invalid_target',
|
||||
message: 'Настройки /pod доступны только главным пользователям',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const parsed = podSettingsBodySchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.code(400).send({
|
||||
error: {
|
||||
code: 'validation_error',
|
||||
message: 'Ожидаются canCreateChildren (boolean) и maxChildren (integer ≥ 0)',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const updatedAt = nowIso()
|
||||
const existing = app.db
|
||||
.select()
|
||||
.from(userPodSettings)
|
||||
.where(eq(userPodSettings.username, username))
|
||||
.get()
|
||||
|
||||
if (existing) {
|
||||
app.db
|
||||
.update(userPodSettings)
|
||||
.set({
|
||||
canCreateChildren: parsed.data.canCreateChildren,
|
||||
maxChildren: parsed.data.maxChildren,
|
||||
updatedAt,
|
||||
})
|
||||
.where(eq(userPodSettings.username, username))
|
||||
.run()
|
||||
} else {
|
||||
app.db
|
||||
.insert(userPodSettings)
|
||||
.values({
|
||||
username,
|
||||
canCreateChildren: parsed.data.canCreateChildren,
|
||||
maxChildren: parsed.data.maxChildren,
|
||||
updatedAt,
|
||||
})
|
||||
.run()
|
||||
}
|
||||
|
||||
const childrenCount = countChildren(app, username)
|
||||
return {
|
||||
username,
|
||||
canCreateChildren: parsed.data.canCreateChildren,
|
||||
maxChildren: parsed.data.maxChildren,
|
||||
childrenCount,
|
||||
remaining: remainingSlots(
|
||||
parsed.data.canCreateChildren,
|
||||
parsed.data.maxChildren,
|
||||
childrenCount,
|
||||
),
|
||||
updatedAt,
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** Best-effort cleanup when a Telemt user is deleted via admin proxy. */
|
||||
export function cleanupUserPanelData(app: FastifyInstance, username: string): void {
|
||||
app.db.delete(userHierarchy).where(eq(userHierarchy.childUsername, username)).run()
|
||||
app.db
|
||||
.delete(userHierarchy)
|
||||
.where(eq(userHierarchy.parentUsername, username))
|
||||
.run()
|
||||
app.db.delete(userPodSettings).where(eq(userPodSettings.username, username)).run()
|
||||
}
|
||||
@@ -5,6 +5,23 @@ import { telemtProxyRequestSchema } from '@telemt/shared'
|
||||
import { agents, jobs, enrollmentTokens, managedClients } from '@telemt/db'
|
||||
import { requireAuth, getOperator } from '../plugins/auth-guards.js'
|
||||
import { sha256, randomBytes } from './auth.js'
|
||||
import { cleanupUserPanelData } from './pod.js'
|
||||
|
||||
function maybeCleanupDeletedUser(
|
||||
app: FastifyInstance,
|
||||
method: string,
|
||||
suffix: string,
|
||||
status: number,
|
||||
): void {
|
||||
if (method !== 'DELETE' || status >= 400) return
|
||||
const match = /^users\/([^/?]+)$/.exec(suffix)
|
||||
if (!match?.[1]) return
|
||||
try {
|
||||
cleanupUserPanelData(app, decodeURIComponent(match[1]))
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
export async function telemtRoutes(app: FastifyInstance) {
|
||||
app.all('/api/telemt/*', { preHandler: requireAuth }, async (request, reply) => {
|
||||
@@ -23,6 +40,7 @@ export async function telemtRoutes(app: FastifyInstance) {
|
||||
body: method === 'GET' || method === 'DELETE' ? undefined : request.body,
|
||||
ifMatch: typeof ifMatch === 'string' ? ifMatch : undefined,
|
||||
})
|
||||
maybeCleanupDeletedUser(app, method, suffix, status)
|
||||
return reply.code(status).send(envelope)
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Telemt unreachable'
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import type { TelemtClient, TelemtEnvelope } from './telemt-client.js'
|
||||
|
||||
export interface TelemtUserLinks {
|
||||
classic?: string[]
|
||||
secure?: string[]
|
||||
tls?: string[]
|
||||
tls_domains?: Array<{ domain?: string; link?: string }>
|
||||
}
|
||||
|
||||
export interface TelemtUserInfo {
|
||||
username: string
|
||||
enabled?: boolean
|
||||
links?: TelemtUserLinks
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** Extract Telemt proxy secret from raw hex or tg:// / t.me proxy URL. */
|
||||
export function extractPodSecret(input: string): string | null {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) return null
|
||||
|
||||
const looksLikeUrl =
|
||||
/^(tg:\/\/|https?:\/\/)/i.test(trimmed) ||
|
||||
/t\.me\/(proxy|socks)/i.test(trimmed)
|
||||
|
||||
if (looksLikeUrl) {
|
||||
try {
|
||||
const normalized = trimmed.replace(/^tg:\/\//i, 'https://tg/')
|
||||
const url = new URL(normalized)
|
||||
const secret = url.searchParams.get('secret')
|
||||
if (secret && secret.trim()) return secret.trim()
|
||||
} catch {
|
||||
const match = /[?&]secret=([^&\s#]+)/i.exec(trimmed)
|
||||
if (match?.[1]) {
|
||||
try {
|
||||
return decodeURIComponent(match[1]).trim()
|
||||
} catch {
|
||||
return match[1].trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return trimmed
|
||||
}
|
||||
|
||||
function collectLinkStrings(links?: TelemtUserLinks): string[] {
|
||||
if (!links) return []
|
||||
const out: string[] = []
|
||||
for (const list of [links.classic, links.secure, links.tls]) {
|
||||
if (Array.isArray(list)) out.push(...list)
|
||||
}
|
||||
for (const row of links.tls_domains ?? []) {
|
||||
if (row?.link) out.push(row.link)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function userLinksContainSecret(
|
||||
user: TelemtUserInfo,
|
||||
secret: string,
|
||||
): boolean {
|
||||
if (!secret) return false
|
||||
return collectLinkStrings(user.links).some((link) => link.includes(secret))
|
||||
}
|
||||
|
||||
function normalizeUsersList(envelope: TelemtEnvelope<unknown>): TelemtUserInfo[] {
|
||||
const data = envelope.data
|
||||
if (Array.isArray(data)) return data as TelemtUserInfo[]
|
||||
if (data && typeof data === 'object' && Array.isArray((data as { users?: unknown }).users)) {
|
||||
return (data as { users: TelemtUserInfo[] }).users
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export async function resolveUserBySecret(
|
||||
telemt: TelemtClient,
|
||||
secretOrLink: string,
|
||||
): Promise<TelemtUserInfo | null> {
|
||||
const secret = extractPodSecret(secretOrLink)
|
||||
if (!secret) return null
|
||||
|
||||
const { status, envelope } = await telemt.request({
|
||||
method: 'GET',
|
||||
path: '/v1/users',
|
||||
})
|
||||
if (status >= 400 || !envelope.ok) return null
|
||||
|
||||
const users = normalizeUsersList(envelope)
|
||||
return users.find((u) => userLinksContainSecret(u, secret)) ?? null
|
||||
}
|
||||
|
||||
export async function fetchTelemtUsers(
|
||||
telemt: TelemtClient,
|
||||
): Promise<TelemtUserInfo[]> {
|
||||
const { status, envelope } = await telemt.request({
|
||||
method: 'GET',
|
||||
path: '/v1/users',
|
||||
})
|
||||
if (status >= 400 || !envelope.ok) return []
|
||||
return normalizeUsersList(envelope)
|
||||
}
|
||||
|
||||
export async function fetchTelemtUser(
|
||||
telemt: TelemtClient,
|
||||
username: string,
|
||||
): Promise<TelemtUserInfo | null> {
|
||||
const { status, envelope } = await telemt.request({
|
||||
method: 'GET',
|
||||
path: `/v1/users/${encodeURIComponent(username)}`,
|
||||
})
|
||||
if (status >= 400 || !envelope.ok) return null
|
||||
const data = envelope.data
|
||||
if (data && typeof data === 'object' && 'username' in (data as object)) {
|
||||
return data as TelemtUserInfo
|
||||
}
|
||||
if (data && typeof data === 'object' && 'user' in (data as object)) {
|
||||
return (data as { user: TelemtUserInfo }).user
|
||||
}
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user