Compare commits

...

82 Commits

Author SHA1 Message Date
chyoo 9f67ee5719 fix(admin): filter non-container releases from deployments
ci/woodpecker/push/woodpecker Pipeline was successful
2026-08-13 17:53:48 +09:00
chyoo a6a4426249 add instructor invite code..
ci/woodpecker/push/woodpecker Pipeline was successful
2026-08-07 11:57:55 +09:00
chyoo 8b7ec648cd add tailscale node delete!
ci/woodpecker/push/woodpecker Pipeline was successful
2026-08-07 10:25:49 +09:00
chyoo 8151ec094d feat: display platform agent status and version
ci/woodpecker/push/woodpecker Pipeline was successful
2026-08-06 16:03:26 +09:00
chyoo 63e85d5306 fix: exclude database components from app releases and deployments 2026-07-15 16:10:36 +09:00
chyoo 3945e42c53 feat: add quick deploy for launcher backend builds
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-15 16:01:09 +09:00
chyoo 53b4909df7 fix: add lms web verision url
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-15 14:55:19 +09:00
chyoo a80e6eab20 fix: apply deployment resource settings to Kubernetes workloads
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-15 14:28:36 +09:00
chyoo 1c240e895b fix: install native build tools for launcher client builds
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-15 14:15:05 +09:00
chyoo 03d065bccb chore: remove stale generated prisma clients
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-14 15:13:39 +09:00
chyoo f4a748b5f3 improve runtime environment variable modal usability
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-14 15:08:50 +09:00
chyoo d922b873e4 add infrastructure resource YAML viewer and fix PVC visibility
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-14 13:35:59 +09:00
chyoo 78f3578098 slim admin backend image and remove compose config
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-14 13:15:21 +09:00
chyoo e811208773 fix: use admin R2 avatars for operator activity..
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-14 11:51:43 +09:00
chyoo cd4bdbecd7 add manual manage..
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-14 10:45:31 +09:00
chyoo e31a07f5d9 add admin issue..
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-13 17:14:19 +09:00
chyoo 29abcca57e launcher deploy modify..
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-13 17:02:51 +09:00
chyoo 195a893bd1 modify nav..
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-13 16:56:13 +09:00
chyoo aac1d01526 add terms management for signup agreements
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-10 14:21:45 +09:00
chyoo e8ea575182 improve license policies and launcher display settings
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-10 13:40:09 +09:00
chyoo ada3309db4 manage license app launch policies
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-10 13:14:29 +09:00
chyoo 6311f8a553 show notice creators with admin avatars
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-10 11:11:32 +09:00
chyoo 609c345739 link admin avatars by email-backed auth users
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-10 10:55:04 +09:00
chyoo 3873ce3169 track requesters for builds releases deployments and migrations
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-10 10:43:02 +09:00
chyoo dd09471267 add runtime profile deletion and refine config forms
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-09 14:53:45 +09:00
chyoo 3a79364905 improve app registration and component service ports
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-09 14:26:49 +09:00
chyoo c46c176ca8 refine app management and source repository displays
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-09 13:50:45 +09:00
chyoo fbefc08b85 add app monitoring charts and runtime metrics
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-09 13:09:41 +09:00
chyoo f142de8763 load full log
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-09 12:54:10 +09:00
chyoo b191dc9f03 refine dashboard infrastructure and resource status
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-09 11:56:05 +09:00
chyoo c1d424b13c refine common app pipelines and user avatars
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-09 11:27:45 +09:00
chyoo ca35c30f87 add avatar user list
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-09 11:20:23 +09:00
chyoo dceb20b26a improve common app pipeline history layout
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-09 11:01:39 +09:00
chyoo 76cc887371 paginate common app pipeline history by repository
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-09 10:53:53 +09:00
chyoo b35c486ae9 show common app pipeline sha and duration
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-09 10:23:37 +09:00
chyoo c2f834e92d improve admin release and deployment safeguards
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-09 09:46:43 +09:00
chyoo da30d88688 admin appPermissions
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-09 09:13:01 +09:00
chyoo eb79bddcc0 admin appPermissions
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-09 09:07:03 +09:00
chyoo beb9dda8c6 connect tailscale node status through platform gateway
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-08 18:14:02 +09:00
chyoo 27bf41d2d3 connect tailscale node status through platform gateway
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-08 17:51:27 +09:00
chyoo 5a2041ddd3 connect tailscale node status through platform gateway
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-08 17:22:46 +09:00
chyoo fddf70c985 connect tailscale node status through platform gateway
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-08 17:13:52 +09:00
chyoo 52d53b0462 connect tailscale node status through platform gateway
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-08 16:39:20 +09:00
chyoo ec17bba92d add launcher component secret setting support
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-08 15:24:15 +09:00
chyoo e74c296b7e harden release promotion fallback on conflict
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-07 18:14:35 +09:00
chyoo ce99890dc8 harden release promotion fallback on conflict 2026-07-07 18:14:12 +09:00
chyoo de98022e2c resolve release promotion unrelated-history conflicts by taking source side
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-07 18:11:42 +09:00
chyoo 6006bcb85e resolve release promotion unrelated-history conflicts by taking source side
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-07 18:09:14 +09:00
chyoo aef91b51bb handle unrelated history promotion fallback
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-07 18:07:30 +09:00
chyoo 89b0b7d15c handle unrelated history promotion fallback
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-07 18:05:13 +09:00
chyoo 85cd5ebcd5 handle unrelated history promotion fallback
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-07 17:59:41 +09:00
chyoo d55fe0c251 paginate app operations and refine runtime settings layout
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-07 17:48:09 +09:00
chyoo 36acfedaee paginate app operations and refine runtime settings layout
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-07 17:42:31 +09:00
chyoo cc5a15a01c paginate app operations and refine runtime settings layout
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-07 17:08:49 +09:00
chyoo 5be7f0980b paginate app operations and refine runtime settings layout
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-07 15:33:08 +09:00
chyoo 895e56088c add organization management and refine source repository view
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-07 11:56:42 +09:00
chyoo 321b7c316e support paged launcher history queries
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-07 09:29:56 +09:00
chyoo 2b46274865 support paged launcher history queries
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-07 09:25:03 +09:00
chyoo 0951283c8b support paged launcher history queries
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-07 09:20:18 +09:00
chyoo b3dbcfd87f load latest woodpecker pipelines for dashboard
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-06 18:32:57 +09:00
chyoo 3aa90d2eee add woodpecker dashboard proxy
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-06 18:21:27 +09:00
chyoo 84c7889857 avoid runtime config rollout on apply
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-06 17:50:14 +09:00
chyoo 7741f02e9b track and retry app release promotions
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-06 17:16:11 +09:00
chyoo 7ca622af4f build app releases from release branch
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-06 16:31:35 +09:00
chyoo 855606a215 add launcher backend deployment api
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-06 14:21:47 +09:00
chyoo 189bf3d8bd update backend package config
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-06 10:41:13 +09:00
chyoo c967f9ef12 ignore TypeScript build info 2026-07-06 10:41:02 +09:00
chyoo bb0fa5c619 inject launcher client build env from settings
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-06 10:39:34 +09:00
chyoo a06d200eac add runtime workload env management api
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-06 10:25:12 +09:00
chyoo 979ccac26d show only active auth sessions
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-06 09:16:27 +09:00
chyoo 6d205a9ad8 add woodpecker release deployment
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-03 16:37:38 +09:00
chyoo f7f5d929ec promote verified develop builds to release 2026-07-03 16:12:14 +09:00
chyoo b56485e9ac promote verified develop builds to release 2026-07-03 16:05:59 +09:00
chyoo dd11e5186a read launcher build version from package json 2026-07-03 10:45:55 +09:00
chyoo fa478b45b3 use traefik ingressroute for runtime ingress 2026-07-02 17:14:34 +09:00
chyoo 4e819ab6de apply ingress rules from runtime profile 2026-07-02 16:19:25 +09:00
chyoo 3dea5f4e5c skip existing database statefulset on runtime apply 2026-07-02 16:12:56 +09:00
chyoo 2729e95a5b add build env and path ingress controls 2026-07-02 15:44:12 +09:00
chyoo 3195d36527 support launcher backend source and image builds 2026-07-02 14:48:25 +09:00
chyoo 3d15bdf6bb use app version in R2 artifact path 2026-07-02 13:51:21 +09:00
chyoo 94ce04a966 add binutils to admin backend image 2026-07-02 13:21:53 +09:00
chyoo 8a610567b2 add launcher user app permission api 2026-07-02 13:21:53 +09:00
144 changed files with 6940 additions and 24649 deletions
+4
View File
@@ -9,6 +9,10 @@ AUTH_SERVER_URL=http://localhost:4100
GITEA_BASE_URL=http://localhost:3000
GITEA_APP_ORG=platform
GITEA_ADMIN_TOKEN=
GITEA_RELEASE_PUSH_WHITELIST=
GITEA_RELEASE_MERGE_WHITELIST=
GITEA_RELEASE_PUSH_WHITELIST_TEAMS=
GITEA_RELEASE_MERGE_WHITELIST_TEAMS=
BUILD_REGISTRY=localhost:5000
BUILD_ARTIFACT_INSTALL_COMMAND=npm ci
LAUNCHER_BUILD_COMMAND="npm ci && npm run build:linux"
BIN
View File
Binary file not shown.
+4
View File
@@ -3,3 +3,7 @@ dist
.env
coverage
uploads
*.swp
tsconfig.build.tsbuildinfo
+31
View File
@@ -0,0 +1,31 @@
steps:
check:
image: node:22-bookworm-slim
commands:
- npm ci
- npm run build
when:
branch: develop
event: push
build-push:
image: docker:27-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- docker build -t localhost:5000/studio-admin-be:0.1.6 .
- docker push localhost:5000/studio-admin-be:0.1.6
when:
branch: release
event: push
deploy:
image: bitnami/kubectl:latest
volumes:
- /home/future2/Studio_v2/woodpecker/kube/config:/tmp/kubeconfig:ro
commands:
- kubectl --kubeconfig=/tmp/kubeconfig -n studio-system rollout restart deployment/studio-admin-be
- kubectl --kubeconfig=/tmp/kubeconfig -n studio-system rollout status deployment/studio-admin-be
when:
branch: release
event: push
+6 -2
View File
@@ -8,11 +8,15 @@ RUN npm run build
FROM node:22-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ARG DOCKER_VERSION=27.5.1
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl git docker.io \
&& apt-get install -y --no-install-recommends ca-certificates curl git python3 make g++ \
&& curl -fsSL "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz" \
| tar -xz -C /tmp \
&& install -m 0755 /tmp/docker/docker /usr/local/bin/docker \
&& curl -fsSL -o /usr/local/bin/kubectl "https://dl.k8s.io/release/v1.30.0/bin/linux/amd64/kubectl" \
&& chmod 0755 /usr/local/bin/kubectl \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /tmp/docker /var/lib/apt/lists/*
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
-21
View File
@@ -1,21 +0,0 @@
services:
postgres:
image: postgres:17-alpine
container_name: atnmy-stdo-adm-v2-postgres
environment:
POSTGRES_DB: studio_admin
POSTGRES_USER: studio_admin
POSTGRES_PASSWORD: future1234!@
ports:
- "5436:5432"
volumes:
- atnmy-stdo-adm-v2-postgres:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U studio_admin -d studio_admin"]
interval: 5s
timeout: 3s
retries: 20
volumes:
atnmy-stdo-adm-v2-postgres:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "atnmy-stdo-adm-v2-be",
"version": "0.1.0",
"version": "0.1.1",
"private": true,
"description": "Backend API for Autonomy Studio Admin",
"license": "UNLICENSED",
+87
View File
@@ -14,6 +14,14 @@ enum UserRole {
USER
}
enum OrganizationType {
COMPANY
SCHOOL
GOVERNMENT
LAB
OTHER
}
enum LicenseCode {
FREE
EDU
@@ -25,6 +33,9 @@ model User {
email String @unique
passwordHash String @map("password_hash")
name String?
profileImageData Bytes? @map("profile_image_data")
profileImageMime String? @map("profile_image_mime")
profileImageUpdatedAt DateTime? @map("profile_image_updated_at") @db.Timestamptz(3)
role UserRole @default(USER)
isActive Boolean @default(true) @map("is_active")
lastLoginAt DateTime? @map("last_login_at") @db.Timestamptz(3)
@@ -32,6 +43,8 @@ model User {
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
userLicenses UserLicense[]
refreshTokens RefreshToken[]
affiliations UserAffiliation[]
appRoles UserAppRole[]
@@map("users")
}
@@ -59,11 +72,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
@@ -79,3 +107,62 @@ model UserLicense {
@@index([licenseId])
@@map("user_licenses")
}
model Organization {
id String @id @default(uuid()) @db.Uuid
name String
type OrganizationType @default(OTHER)
domain String?
defaultLicenseCode String? @map("default_license_code")
instructorInviteCode String @unique @map("instructor_invite_code")
isActive Boolean @default(true) @map("is_active")
createdById String? @map("created_by_id") @db.Uuid
createdByName String? @map("created_by_name")
createdByEmail String? @map("created_by_email")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
affiliations UserAffiliation[]
@@unique([name, type])
@@index([isActive, type])
@@index([createdByEmail])
@@map("organizations")
}
model UserAffiliation {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
organizationId String @map("organization_id") @db.Uuid
department String?
position String?
isPrimary Boolean @default(false) @map("is_primary")
startsAt DateTime? @map("starts_at") @db.Timestamptz(3)
endsAt DateTime? @map("ends_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Restrict)
@@index([userId, isPrimary])
@@index([organizationId])
@@map("user_affiliations")
}
model UserAppRole {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
appKey String @map("app_key")
roleCode String @map("role_code")
assignedById String? @map("assigned_by") @db.Uuid
assignedAt DateTime @default(now()) @map("assigned_at") @db.Timestamptz(3)
startsAt DateTime? @map("starts_at") @db.Timestamptz(3)
endsAt DateTime? @map("ends_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, appKey, roleCode])
@@index([appKey, roleCode])
@@index([userId, appKey])
@@map("user_app_roles")
}
+65 -2
View File
@@ -19,13 +19,50 @@ model LauncherNotice {
startsAt DateTime? @map("starts_at") @db.Timestamptz(3)
endsAt DateTime? @map("ends_at") @db.Timestamptz(3)
priority Int @default(0)
createdById String? @map("created_by_id") @db.Uuid
createdByName String? @map("created_by_name")
createdByEmail String? @map("created_by_email")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
@@index([appKey, isActive, priority])
@@index([createdByEmail])
@@map("launcher_notices")
}
model LauncherTerm {
id String @id @default(uuid()) @db.Uuid
key String @unique
body String
isRequired Boolean @default(true) @map("is_required")
createdById String? @map("created_by_id") @db.Uuid
createdByName String? @map("created_by_name")
createdByEmail String? @map("created_by_email")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
@@index([createdByEmail])
@@map("launcher_terms")
}
model LauncherManualDocument {
id String @id @default(uuid()) @db.Uuid
appKey String @map("app_key")
title String
body String
isPublished Boolean @default(true) @map("is_published")
sortOrder Int @default(0) @map("sort_order")
createdById String? @map("created_by_id") @db.Uuid
createdByName String? @map("created_by_name")
createdByEmail String? @map("created_by_email")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
@@index([appKey, isPublished, sortOrder])
@@index([createdByEmail])
@@map("launcher_manual_documents")
}
model ProvisionedDevice {
id String @id @default(uuid()) @db.Uuid
accountEmail String @map("account_email")
@@ -40,6 +77,9 @@ model ProvisionedDevice {
kernel String?
distro String?
device Json?
agentVersion String? @map("agent_version")
agentUpdatedAt DateTime? @map("agent_updated_at") @db.Timestamptz(3)
agentLastSeenAt DateTime? @map("agent_last_seen_at") @db.Timestamptz(3)
installedAt DateTime @default(now()) @map("installed_at") @db.Timestamptz(3)
lastProvisionedAt DateTime @default(now()) @map("last_provisioned_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
@@ -48,9 +88,32 @@ model ProvisionedDevice {
@@unique([accountEmail, tailscaleHostname])
@@index([accountEmail])
@@index([tailscaleHostname])
@@index([agentLastSeenAt])
@@map("provisioned_devices")
}
model DeviceAuthorization {
id String @id @default(uuid()) @db.Uuid
deviceCodeHash String @unique @map("device_code_hash")
userCode String @unique @map("user_code")
status String @default("PENDING")
authUserId String? @map("auth_user_id") @db.Uuid
accountEmail String? @map("account_email")
authUserName String? @map("auth_user_name")
authUserRole String? @map("auth_user_role")
authUserRoles Json? @map("auth_user_roles")
device Json?
expiresAt DateTime @map("expires_at") @db.Timestamptz(3)
approvedAt DateTime? @map("approved_at") @db.Timestamptz(3)
consumedAt DateTime? @map("consumed_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
@@index([status, expiresAt])
@@index([authUserId, createdAt])
@@map("device_authorizations")
}
model LauncherSshKey {
id String @id @default(uuid()) @db.Uuid
accountEmail String @map("account_email")
@@ -69,7 +132,7 @@ model LauncherSshKey {
}
model LauncherCommonSetting {
model LauncherSetting {
id String @id @default(uuid()) @db.Uuid
key String @unique
name String
@@ -82,5 +145,5 @@ model LauncherCommonSetting {
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
@@index([type, isActive])
@@map("launcher_common_settings")
@@map("launcher_settings")
}
@@ -0,0 +1,25 @@
CREATE TABLE "build_env_variables" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"component_id" UUID NOT NULL,
"key" TEXT NOT NULL,
"value" TEXT NOT NULL,
"is_secret" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "build_env_variables_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "build_env_variables_component_id_key_key" ON "build_env_variables"("component_id", "key");
CREATE INDEX "build_env_variables_component_id_idx" ON "build_env_variables"("component_id");
CREATE INDEX "build_env_variables_component_id_is_secret_idx" ON "build_env_variables"("component_id", "is_secret");
ALTER TABLE "build_env_variables"
ADD CONSTRAINT "build_env_variables_component_id_fkey"
FOREIGN KEY ("component_id") REFERENCES "app_components"("id") ON DELETE CASCADE ON UPDATE CASCADE;
COMMENT ON TABLE "build_env_variables" IS '구성요소 빌드 시 주입할 환경변수';
COMMENT ON COLUMN "build_env_variables"."component_id" IS '빌드 대상 구성요소 ID';
COMMENT ON COLUMN "build_env_variables"."key" IS '환경변수 키';
COMMENT ON COLUMN "build_env_variables"."value" IS '환경변수 값';
COMMENT ON COLUMN "build_env_variables"."is_secret" IS '비밀값 여부';
@@ -0,0 +1,4 @@
ALTER TABLE "runtime_ingress_rules"
ADD COLUMN "strip_prefix" BOOLEAN NOT NULL DEFAULT false;
COMMENT ON COLUMN "runtime_ingress_rules"."strip_prefix" IS 'Traefik StripPrefix 미들웨어 적용 여부';
@@ -0,0 +1,3 @@
ALTER TABLE "app_releases" ADD COLUMN "logs" TEXT;
COMMENT ON COLUMN "app_releases"."logs" IS '릴리즈 생성 및 release 빌드 로그';
@@ -0,0 +1,8 @@
ALTER TABLE IF EXISTS "launcher_common_settings" RENAME TO "launcher_settings";
DO $$
BEGIN
IF to_regclass('public.launcher_settings') IS NOT NULL THEN
COMMENT ON TABLE "launcher_settings" IS '런처 소스, 빌드 환경 등 내부 설정';
END IF;
END $$;
@@ -0,0 +1,36 @@
CREATE TABLE IF NOT EXISTS "admin_app_permissions" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"admin_id" UUID NOT NULL,
"app_id" UUID NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "admin_app_permissions_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "admin_app_permissions_admin_id_app_id_key"
ON "admin_app_permissions"("admin_id", "app_id");
CREATE INDEX IF NOT EXISTS "admin_app_permissions_app_id_idx"
ON "admin_app_permissions"("app_id");
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'admin_app_permissions_admin_id_fkey'
) THEN
ALTER TABLE "admin_app_permissions"
ADD CONSTRAINT "admin_app_permissions_admin_id_fkey"
FOREIGN KEY ("admin_id") REFERENCES "admins"("id") ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'admin_app_permissions_app_id_fkey'
) THEN
ALTER TABLE "admin_app_permissions"
ADD CONSTRAINT "admin_app_permissions_app_id_fkey"
FOREIGN KEY ("app_id") REFERENCES "apps"("id") ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
@@ -0,0 +1,32 @@
DO $$
BEGIN
CREATE TYPE "AdminRole" AS ENUM ('OWNER', 'ADMIN', 'OPERATOR', 'VIEWER');
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
CREATE TABLE IF NOT EXISTS "admin_roles" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"code" "AdminRole" NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "admin_roles_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "admin_roles_code_key"
ON "admin_roles"("code");
CREATE INDEX IF NOT EXISTS "admin_roles_is_active_sort_order_idx"
ON "admin_roles"("is_active", "sort_order");
INSERT INTO "admin_roles" ("id", "code", "name", "description", "sort_order", "is_active", "updated_at")
VALUES
(gen_random_uuid(), 'OWNER', 'OWNER', '전체 관리자 권한', 1, true, CURRENT_TIMESTAMP),
(gen_random_uuid(), 'ADMIN', 'ADMIN', '관리자 권한', 2, true, CURRENT_TIMESTAMP),
(gen_random_uuid(), 'VIEWER', 'VIEWER', '조회 권한', 3, true, CURRENT_TIMESTAMP)
ON CONFLICT ("code") DO NOTHING;
@@ -0,0 +1,5 @@
ALTER TABLE "build_jobs"
ADD COLUMN IF NOT EXISTS "release_candidate_hidden_at" TIMESTAMPTZ(3);
CREATE INDEX IF NOT EXISTS "build_jobs_release_candidate_hidden_at_idx"
ON "build_jobs"("release_candidate_hidden_at");
@@ -0,0 +1,11 @@
ALTER TABLE "launcher_build_jobs"
ADD COLUMN "requested_by_id" UUID,
ADD COLUMN "requested_by_name" TEXT,
ADD COLUMN "requested_by_email" TEXT;
CREATE INDEX IF NOT EXISTS "launcher_build_jobs_requested_by_id_idx"
ON "launcher_build_jobs"("requested_by_id");
COMMENT ON COLUMN "launcher_build_jobs"."requested_by_id" IS '빌드 작업을 요청한 관리자 ID';
COMMENT ON COLUMN "launcher_build_jobs"."requested_by_name" IS '빌드 작업을 요청한 관리자 이름';
COMMENT ON COLUMN "launcher_build_jobs"."requested_by_email" IS '빌드 작업을 요청한 관리자 이메일';
@@ -0,0 +1,41 @@
ALTER TABLE "build_jobs"
ADD COLUMN "requested_by_id" UUID,
ADD COLUMN "requested_by_name" TEXT,
ADD COLUMN "requested_by_email" TEXT;
ALTER TABLE "app_releases"
ADD COLUMN "requested_by_id" UUID,
ADD COLUMN "requested_by_name" TEXT,
ADD COLUMN "requested_by_email" TEXT;
ALTER TABLE "deployment_jobs"
ADD COLUMN "requested_by_id" UUID,
ADD COLUMN "requested_by_name" TEXT,
ADD COLUMN "requested_by_email" TEXT;
ALTER TABLE "runtime_control_jobs"
ADD COLUMN "requested_by_id" UUID,
ADD COLUMN "requested_by_name" TEXT,
ADD COLUMN "requested_by_email" TEXT;
CREATE INDEX IF NOT EXISTS "build_jobs_requested_by_id_idx"
ON "build_jobs"("requested_by_id");
CREATE INDEX IF NOT EXISTS "app_releases_requested_by_id_idx"
ON "app_releases"("requested_by_id");
CREATE INDEX IF NOT EXISTS "deployment_jobs_requested_by_id_idx"
ON "deployment_jobs"("requested_by_id");
CREATE INDEX IF NOT EXISTS "runtime_control_jobs_requested_by_id_idx"
ON "runtime_control_jobs"("requested_by_id");
COMMENT ON COLUMN "build_jobs"."requested_by_id" IS '작업을 요청한 관리자 ID';
COMMENT ON COLUMN "build_jobs"."requested_by_name" IS '작업을 요청한 관리자 이름';
COMMENT ON COLUMN "build_jobs"."requested_by_email" IS '작업을 요청한 관리자 이메일';
COMMENT ON COLUMN "app_releases"."requested_by_id" IS '작업을 요청한 관리자 ID';
COMMENT ON COLUMN "app_releases"."requested_by_name" IS '작업을 요청한 관리자 이름';
COMMENT ON COLUMN "app_releases"."requested_by_email" IS '작업을 요청한 관리자 이메일';
COMMENT ON COLUMN "deployment_jobs"."requested_by_id" IS '작업을 요청한 관리자 ID';
COMMENT ON COLUMN "deployment_jobs"."requested_by_name" IS '작업을 요청한 관리자 이름';
COMMENT ON COLUMN "deployment_jobs"."requested_by_email" IS '작업을 요청한 관리자 이메일';
COMMENT ON COLUMN "runtime_control_jobs"."requested_by_id" IS '작업을 요청한 관리자 ID';
COMMENT ON COLUMN "runtime_control_jobs"."requested_by_name" IS '작업을 요청한 관리자 이름';
COMMENT ON COLUMN "runtime_control_jobs"."requested_by_email" IS '작업을 요청한 관리자 이메일';
@@ -0,0 +1,16 @@
DO $$
BEGIN
IF to_regclass('public.launcher_notices') IS NOT NULL THEN
ALTER TABLE "launcher_notices"
ADD COLUMN IF NOT EXISTS "created_by_id" UUID,
ADD COLUMN IF NOT EXISTS "created_by_name" TEXT,
ADD COLUMN IF NOT EXISTS "created_by_email" TEXT;
END IF;
END $$;
DO $$
BEGIN
IF to_regclass('public.launcher_notices') IS NOT NULL THEN
CREATE INDEX IF NOT EXISTS "launcher_notices_created_by_email_idx" ON "launcher_notices"("created_by_email");
END IF;
END $$;
@@ -0,0 +1,7 @@
DO $$
BEGIN
IF to_regclass('public.organizations') IS NOT NULL THEN
ALTER TABLE "organizations"
ADD COLUMN IF NOT EXISTS "default_license_code" TEXT;
END IF;
END $$;
@@ -0,0 +1,19 @@
DO $$
BEGIN
IF to_regclass('public.licenses') IS NOT NULL THEN
CREATE TABLE IF NOT EXISTS "license_app_permissions" (
"id" UUID NOT NULL,
"license_id" UUID NOT NULL,
"app_key" TEXT NOT NULL,
"can_launch" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "license_app_permissions_pkey" PRIMARY KEY ("id"),
CONSTRAINT "license_app_permissions_license_id_fkey" FOREIGN KEY ("license_id") REFERENCES "licenses"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS "license_app_permissions_license_id_app_key_key" ON "license_app_permissions"("license_id", "app_key");
CREATE INDEX IF NOT EXISTS "license_app_permissions_app_key_idx" ON "license_app_permissions"("app_key");
END IF;
END $$;
@@ -0,0 +1,28 @@
DO $$
BEGIN
IF to_regclass('public.launcher_notices') IS NOT NULL THEN
CREATE TABLE IF NOT EXISTS "launcher_terms" (
"id" UUID NOT NULL,
"key" TEXT NOT NULL,
"body" TEXT NOT NULL,
"is_required" BOOLEAN NOT NULL DEFAULT true,
"created_by_id" UUID,
"created_by_name" TEXT,
"created_by_email" TEXT,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "launcher_terms_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "launcher_terms_key_key" ON "launcher_terms"("key");
CREATE INDEX IF NOT EXISTS "launcher_terms_created_by_email_idx" ON "launcher_terms"("created_by_email");
ALTER TABLE "launcher_terms" DROP COLUMN IF EXISTS "title";
ALTER TABLE "launcher_terms" DROP COLUMN IF EXISTS "priority";
ALTER TABLE "launcher_terms" DROP COLUMN IF EXISTS "version";
ALTER TABLE "launcher_terms" DROP COLUMN IF EXISTS "is_active";
ALTER TABLE "launcher_terms" DROP COLUMN IF EXISTS "starts_at";
ALTER TABLE "launcher_terms" DROP COLUMN IF EXISTS "ends_at";
END IF;
END $$;
@@ -0,0 +1,15 @@
CREATE TABLE "launcher_manual_documents" (
"id" UUID NOT NULL,
"key" TEXT NOT NULL,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"is_published" BOOLEAN NOT NULL DEFAULT true,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "launcher_manual_documents_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "launcher_manual_documents_key_key" ON "launcher_manual_documents"("key");
CREATE INDEX "launcher_manual_documents_is_published_sort_order_idx" ON "launcher_manual_documents"("is_published", "sort_order");
@@ -0,0 +1,6 @@
ALTER TABLE "launcher_manual_documents"
ADD COLUMN "app_key" TEXT NOT NULL DEFAULT 'Launcher';
DROP INDEX IF EXISTS "launcher_manual_documents_is_published_sort_order_idx";
CREATE INDEX "launcher_manual_documents_app_key_is_published_sort_order_idx"
ON "launcher_manual_documents"("app_key", "is_published", "sort_order");
@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS "launcher_manual_documents_key_key";
ALTER TABLE "launcher_manual_documents" DROP COLUMN IF EXISTS "key";
@@ -0,0 +1,7 @@
ALTER TABLE "launcher_manual_documents"
ADD COLUMN IF NOT EXISTS "created_by_id" UUID,
ADD COLUMN IF NOT EXISTS "created_by_name" TEXT,
ADD COLUMN IF NOT EXISTS "created_by_email" TEXT;
CREATE INDEX IF NOT EXISTS "launcher_manual_documents_created_by_email_idx"
ON "launcher_manual_documents"("created_by_email");
@@ -0,0 +1,9 @@
DO $$
BEGIN
IF to_regclass('public.organizations') IS NOT NULL THEN
ALTER TABLE "organizations"
ADD COLUMN IF NOT EXISTS "created_by_id" UUID,
ADD COLUMN IF NOT EXISTS "created_by_name" TEXT,
ADD COLUMN IF NOT EXISTS "created_by_email" TEXT;
END IF;
END $$;
@@ -0,0 +1,2 @@
ALTER TABLE "build_jobs"
ADD COLUMN "artifact_sha256" TEXT;
+52
View File
@@ -74,11 +74,25 @@ model Admin {
sessions AdminSession[]
auditLogs AdminAuditLog[]
sshKeys AdminSshKey[]
appPermissions AdminAppPermission[]
@@index([isActive, role])
@@map("admins")
}
model AdminAppPermission {
id String @id @default(uuid()) @db.Uuid
adminId String @map("admin_id") @db.Uuid
appId String @map("app_id") @db.Uuid
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
admin Admin @relation(fields: [adminId], references: [id], onDelete: Cascade)
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
@@unique([adminId, appId])
@@index([appId])
@@map("admin_app_permissions")
}
model AdminSshKey {
id String @id @default(uuid()) @db.Uuid
adminId String @map("admin_id") @db.Uuid
@@ -142,6 +156,7 @@ model App {
releases AppRelease[]
deployments DeploymentJob[]
runtimeProfiles RuntimeEnvProfile[]
adminPermissions AdminAppPermission[]
@@map("apps")
}
@@ -170,6 +185,7 @@ model AppComponent {
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
buildJobs BuildJob[]
buildVariables BuildEnvVariable[]
runtimeVariables RuntimeEnvVariable[]
ingressRules RuntimeIngressRule[]
@@ -177,21 +193,43 @@ model AppComponent {
@@map("app_components")
}
model BuildEnvVariable {
id String @id @default(uuid()) @db.Uuid
componentId String @map("component_id") @db.Uuid
key String
value String
isSecret Boolean @default(false) @map("is_secret")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
component AppComponent @relation(fields: [componentId], references: [id], onDelete: Cascade)
@@unique([componentId, key])
@@index([componentId])
@@index([componentId, isSecret])
@@map("build_env_variables")
}
model BuildJob {
id String @id @default(uuid()) @db.Uuid
componentId String @map("component_id") @db.Uuid
branch String
commitSha String? @map("commit_sha")
artifact String?
artifactSha256 String? @map("artifact_sha256")
image String?
status JobStatus @default(PENDING)
requestedById String? @map("requested_by_id") @db.Uuid
requestedByName String? @map("requested_by_name")
requestedByEmail String? @map("requested_by_email")
logs String?
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
finishedAt DateTime? @map("finished_at") @db.Timestamptz(3)
releaseCandidateHiddenAt DateTime? @map("release_candidate_hidden_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
component AppComponent @relation(fields: [componentId], references: [id], onDelete: Cascade)
@@index([componentId, createdAt])
@@index([releaseCandidateHiddenAt])
@@map("build_jobs")
}
@@ -204,6 +242,10 @@ model AppRelease {
artifact String?
imageTags Json? @map("image_tags")
status JobStatus @default(PENDING)
requestedById String? @map("requested_by_id") @db.Uuid
requestedByName String? @map("requested_by_name")
requestedByEmail String? @map("requested_by_email")
logs String?
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
@@ -221,6 +263,9 @@ model DeploymentJob {
namespace String
status JobStatus @default(PENDING)
requestedBy String? @map("requested_by")
requestedById String? @map("requested_by_id") @db.Uuid
requestedByName String? @map("requested_by_name")
requestedByEmail String? @map("requested_by_email")
logs String?
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
finishedAt DateTime? @map("finished_at") @db.Timestamptz(3)
@@ -243,6 +288,9 @@ model LauncherBuildJob {
uploadPath String @map("upload_path")
downloadUrl String @map("download_url")
status JobStatus @default(PENDING)
requestedById String? @map("requested_by_id") @db.Uuid
requestedByName String? @map("requested_by_name")
requestedByEmail String? @map("requested_by_email")
logs String?
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
finishedAt DateTime? @map("finished_at") @db.Timestamptz(3)
@@ -264,6 +312,9 @@ model RuntimeControlJob {
action RuntimeAction
status JobStatus @default(PENDING)
requestedBy String? @map("requested_by")
requestedById String? @map("requested_by_id") @db.Uuid
requestedByName String? @map("requested_by_name")
requestedByEmail String? @map("requested_by_email")
request Json?
result Json?
logs String?
@@ -319,6 +370,7 @@ model RuntimeIngressRule {
host String
path String @default("/")
servicePort Int @map("service_port")
stripPrefix Boolean @default(false) @map("strip_prefix")
tlsEnabled Boolean @default(false) @map("tls_enabled")
tlsSecretName String? @map("tls_secret_name")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
+40 -2
View File
@@ -1,13 +1,39 @@
import { Body, Controller, Post, Req } from '@nestjs/common';
import { Body, Controller, Delete, Get, Patch, Post, Put, Req } from '@nestjs/common';
import type { Request } from 'express';
import { AdminAuthService } from './admin-auth.service';
import { Public } from './decorators/admin-auth.decorators';
import { AdminLoginDto } from './dto';
import { AdminLoginDto, ChangeAdminPasswordDto } from './dto';
@Controller('auth/admin')
export class AdminAuthController {
constructor(private readonly adminAuthService: AdminAuthService) {}
@Get('me')
me(@Req() request: { admin?: { id: string } }) {
return this.adminAuthService.me(request.admin!.id);
}
@Put('avatar')
async updateAvatar(@Req() request: Request & { admin?: { id: string } }) {
const mimeType = String(request.headers['content-type'] ?? '').split(';')[0].trim().toLowerCase();
const imageData = await readRequestBody(request);
return this.adminAuthService.updateAvatar(request.admin!.id, imageData, mimeType);
}
@Delete('avatar')
deleteAvatar(@Req() request: { admin?: { id: string } }) {
return this.adminAuthService.deleteAvatar(request.admin!.id);
}
@Patch('password')
changePassword(
@Req() request: { admin?: { id: string } },
@Body() body: ChangeAdminPasswordDto,
) {
return this.adminAuthService.changePassword(request.admin!.id, body);
}
@Public()
@Post('login')
login(
@@ -22,3 +48,15 @@ export class AdminAuthController {
});
}
}
function readRequestBody(request: Request) {
return new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
request.on('data', (chunk: Buffer | string) => {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
request.on('end', () => resolve(Buffer.concat(chunks)));
request.on('error', reject);
});
}
+2 -1
View File
@@ -1,11 +1,12 @@
import { Module } from '@nestjs/common';
import { GiteaModule } from '../gitea/gitea.module';
import { PrismaModule } from '../prisma/prisma.module';
import { AdminAuthController } from './admin-auth.controller';
import { AdminAuthService } from './admin-auth.service';
@Module({
imports: [PrismaModule],
imports: [GiteaModule, PrismaModule],
controllers: [AdminAuthController],
providers: [AdminAuthService],
})
+271 -3
View File
@@ -1,13 +1,22 @@
import { randomBytes, scryptSync, timingSafeEqual, createHash } from 'node:crypto';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { DeleteObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { BadRequestException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GiteaService } from '../gitea/gitea.service';
import { PrismaService } from '../prisma/prisma.service';
import { AdminLoginDto } from './dto';
import { AdminLoginDto, ChangeAdminPasswordDto } from './dto';
@Injectable()
export class AdminAuthService {
constructor(private readonly prisma: PrismaService) {}
private r2Client?: S3Client;
constructor(
private readonly configService: ConfigService,
private readonly giteaService: GiteaService,
private readonly prisma: PrismaService,
) {}
async login(
body: AdminLoginDto,
@@ -22,6 +31,19 @@ export class AdminAuthService {
role: true,
isActive: true,
passwordHash: true,
appPermissions: {
select: { appId: true },
},
sshKeys: {
orderBy: { createdAt: 'desc' },
select: {
id: true,
label: true,
fingerprint: true,
giteaUsername: true,
createdAt: true,
},
},
},
});
@@ -49,6 +71,8 @@ export class AdminAuthService {
}),
]);
const avatarUrl = this.getAdminAvatarUrl(admin.id);
return {
token,
expiresAt,
@@ -57,9 +81,235 @@ export class AdminAuthService {
email: admin.email,
name: admin.name,
role: admin.role,
avatarUrl,
appPermissions: admin.appPermissions.map((permission) => permission.appId),
sshKeys: admin.sshKeys,
},
};
}
async me(adminId: string) {
const admin = await this.prisma.admin.findUnique({
where: { id: adminId },
select: {
id: true,
email: true,
name: true,
role: true,
appPermissions: {
select: { appId: true },
},
sshKeys: {
orderBy: { createdAt: 'desc' },
select: {
id: true,
label: true,
fingerprint: true,
giteaUsername: true,
createdAt: true,
},
},
},
});
if (!admin) {
throw new UnauthorizedException('관리자 세션이 유효하지 않습니다.');
}
return {
admin: {
id: admin.id,
email: admin.email,
name: admin.name,
role: admin.role,
avatarUrl: this.getAdminAvatarUrl(admin.id),
appPermissions: admin.appPermissions.map((permission) => permission.appId),
sshKeys: admin.sshKeys,
},
};
}
async updateAvatar(adminId: string, imageData: Buffer, mimeType: string) {
if (!['image/jpeg', 'image/png', 'image/webp'].includes(mimeType)) {
throw new BadRequestException('지원하지 않는 이미지 형식입니다.');
}
if (!imageData.length) {
throw new BadRequestException('이미지 파일이 비어 있습니다.');
}
if (imageData.length > 80 * 1024) {
throw new BadRequestException('프로필 이미지는 80KB 이하로 업로드해 주세요.');
}
const admin = await this.getAdminForAvatar(adminId);
const giteaUsername = await this.ensureGiteaUser(admin);
await this.uploadAvatarToR2(admin.id, imageData, mimeType);
await this.giteaService.updateUserAvatar(giteaUsername, imageData, mimeType);
return {
avatarUrl: this.getAdminAvatarUrl(admin.id),
};
}
async deleteAvatar(adminId: string) {
const admin = await this.getAdminForAvatar(adminId);
const giteaUsername = this.resolveGiteaUsername(admin);
if (this.isR2Enabled()) {
await this.deleteAvatarFromR2(admin.id);
}
await this.giteaService.deleteUserAvatar(giteaUsername);
return {
avatarUrl: this.getAdminAvatarUrl(admin.id),
};
}
async changePassword(adminId: string, body: ChangeAdminPasswordDto) {
if (body.currentPassword === body.newPassword) {
throw new BadRequestException('새 비밀번호는 현재 비밀번호와 달라야 합니다.');
}
const admin = await this.prisma.admin.findUnique({
where: { id: adminId },
select: {
id: true,
passwordHash: true,
isActive: true,
},
});
if (!admin || !admin.isActive) {
throw new UnauthorizedException('관리자 세션이 유효하지 않습니다.');
}
if (!verifyPassword(body.currentPassword, admin.passwordHash)) {
throw new BadRequestException('현재 비밀번호가 올바르지 않습니다.');
}
await this.prisma.admin.update({
where: { id: admin.id },
data: { passwordHash: hashPassword(body.newPassword) },
});
return { ok: true };
}
private getAdminAvatarUrl(adminId: string) {
const publicBaseUrl = this.configService.get<string>('R2_PUBLIC_BASE_URL')?.replace(/\/+$/, '');
if (!publicBaseUrl) return null;
return `${publicBaseUrl}/${this.createAvatarObjectKey(adminId)}`;
}
private async getAdminForAvatar(adminId: string) {
const admin = await this.prisma.admin.findUnique({
where: { id: adminId },
select: {
id: true,
email: true,
name: true,
sshKeys: {
where: { giteaUsername: { not: null } },
orderBy: { createdAt: 'desc' },
take: 1,
select: { giteaUsername: true },
},
},
});
if (!admin) {
throw new UnauthorizedException('관리자 세션이 유효하지 않습니다.');
}
return admin;
}
private async ensureGiteaUser(admin: { email: string; name: string; sshKeys?: Array<{ giteaUsername: string | null }> }) {
const storedUsername = this.resolveStoredGiteaUsername(admin);
if (storedUsername) return storedUsername;
const username = createGiteaUsername(admin.email);
await this.giteaService.createUser({
username,
email: admin.email,
password: randomBytes(24).toString('hex'),
fullName: admin.name,
});
return username;
}
private resolveGiteaUsername(admin: { email: string; sshKeys?: Array<{ giteaUsername: string | null }> }) {
return this.resolveStoredGiteaUsername(admin) ?? createGiteaUsername(admin.email);
}
private resolveStoredGiteaUsername(admin: { sshKeys?: Array<{ giteaUsername: string | null }> }) {
return admin.sshKeys?.find((key) => key.giteaUsername?.trim())?.giteaUsername?.trim() || null;
}
private async uploadAvatarToR2(adminId: string, imageData: Buffer, mimeType: string) {
const bucket = this.configService.get<string>('R2_BUCKET');
const accessKeyId = this.configService.get<string>('R2_ACCESS_KEY_ID');
const secretAccessKey = this.configService.get<string>('R2_SECRET_ACCESS_KEY');
if (!bucket || !accessKeyId || !secretAccessKey) {
throw new BadRequestException('R2 설정이 필요합니다.');
}
await this.getR2Client(accessKeyId, secretAccessKey).send(new PutObjectCommand({
Bucket: bucket,
Key: this.createAvatarObjectKey(adminId),
Body: imageData,
ContentType: mimeType,
CacheControl: 'public, max-age=60',
}));
}
private async deleteAvatarFromR2(adminId: string) {
const bucket = this.configService.get<string>('R2_BUCKET');
const accessKeyId = this.configService.get<string>('R2_ACCESS_KEY_ID');
const secretAccessKey = this.configService.get<string>('R2_SECRET_ACCESS_KEY');
if (!bucket || !accessKeyId || !secretAccessKey) return;
await this.getR2Client(accessKeyId, secretAccessKey).send(new DeleteObjectCommand({
Bucket: bucket,
Key: this.createAvatarObjectKey(adminId),
}));
}
private getR2Client(accessKeyId: string, secretAccessKey: string) {
if (this.r2Client) return this.r2Client;
const endpoint = this.configService.get<string>('R2_ENDPOINT')
|| `https://${this.configService.get<string>('R2_ACCOUNT_ID')}.r2.cloudflarestorage.com`;
this.r2Client = new S3Client({
region: this.configService.get<string>('R2_REGION', 'auto'),
endpoint,
credentials: {
accessKeyId,
secretAccessKey,
},
forcePathStyle: this.configService.get<string>('R2_FORCE_PATH_STYLE', 'true') !== 'false',
});
return this.r2Client;
}
private isR2Enabled() {
return Boolean(
this.configService.get<string>('R2_BUCKET')
&& this.configService.get<string>('R2_ACCESS_KEY_ID')
&& this.configService.get<string>('R2_SECRET_ACCESS_KEY'),
);
}
private createAvatarObjectKey(adminId: string) {
const prefix = this.configService.get<string>('R2_PUBLIC_FILE_PREFIX', 'studio-files/public');
return `${prefix.replace(/^\/+|\/+$/g, '')}/admins/${adminId}/avatar`;
}
}
function verifyPassword(password: string, passwordHash: string) {
@@ -81,3 +331,21 @@ function verifyPassword(password: string, passwordHash: string) {
function hashToken(token: string) {
return createHash('sha256').update(token).digest('hex');
}
function hashPassword(password: string) {
const salt = randomBytes(16).toString('hex');
const hash = scryptSync(password, salt, 64).toString('hex');
return `scrypt:${salt}:${hash}`;
}
function createGiteaUsername(email: string) {
const account = email.split('@')[0] || email;
const username = account
.trim()
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, '-')
.replace(/^[._-]+|[._-]+$/g, '')
.slice(0, 40);
return username || `admin-${createHash('sha1').update(email).digest('hex').slice(0, 8)}`;
}
+10 -1
View File
@@ -1,4 +1,4 @@
import { IsEmail, IsString } from 'class-validator';
import { IsEmail, IsString, MinLength } from 'class-validator';
export class AdminLoginDto {
@IsEmail()
@@ -7,3 +7,12 @@ export class AdminLoginDto {
@IsString()
password!: string;
}
export class ChangeAdminPasswordDto {
@IsString()
currentPassword!: string;
@IsString()
@MinLength(8)
newPassword!: string;
}
+9
View File
@@ -0,0 +1,9 @@
import { ConfigService } from '@nestjs/config';
export function getAdminAvatarUrl(adminId: string | null | undefined, configService: ConfigService) {
const publicBaseUrl = configService.get<string>('R2_PUBLIC_BASE_URL')?.replace(/\/+$/, '');
if (!adminId || !publicBaseUrl) return null;
const prefix = configService.get<string>('R2_PUBLIC_FILE_PREFIX', 'studio-files/public');
return `${publicBaseUrl}/${prefix.replace(/^\/+|\/+$/g, '')}/admins/${adminId}/avatar`;
}
+15 -3
View File
@@ -1,9 +1,11 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
import {
CreateAdminSshKeyDto,
CreateAdminDto,
ListAdminsQuery,
ResetAdminPasswordDto,
UpdateAdminAppPermissionsDto,
UpdateAdminRoleDto,
UpdateAdminStatusDto,
} from './dto';
@@ -22,8 +24,8 @@ export class AdminsController {
}
@Get()
list() {
return this.adminsService.list();
list(@Query() query: ListAdminsQuery) {
return this.adminsService.list(query);
}
@Post()
@@ -46,6 +48,16 @@ export class AdminsController {
return this.adminsService.resetPassword(id, body);
}
@Get(':id/app-permissions')
listAppPermissions(@Param('id') id: string) {
return this.adminsService.listAppPermissions(id);
}
@Put(':id/app-permissions')
updateAppPermissions(@Param('id') id: string, @Body() body: UpdateAdminAppPermissionsDto) {
return this.adminsService.updateAppPermissions(id, body);
}
@Post(':id/ssh-keys')
createSshKey(@Param('id') id: string, @Body() body: CreateAdminSshKeyDto) {
return this.adminsService.createSshKey(id, body);
+135 -11
View File
@@ -1,14 +1,18 @@
import { createHash, randomBytes, scryptSync } from 'node:crypto';
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GiteaService } from '../gitea/gitea.service';
import { PrismaService } from '../prisma/prisma.service';
import { getAdminAvatarUrl } from './admin-avatar';
import { AdminRole } from './admin-role';
import {
CreateAdminSshKeyDto,
CreateAdminDto,
ListAdminsQuery,
ResetAdminPasswordDto,
UpdateAdminAppPermissionsDto,
UpdateAdminRoleDto,
UpdateAdminStatusDto,
} from './dto';
@@ -16,6 +20,7 @@ import {
@Injectable()
export class AdminsService {
constructor(
private readonly configService: ConfigService,
private readonly giteaService: GiteaService,
private readonly prisma: PrismaService,
) {}
@@ -37,9 +42,29 @@ export class AdminsService {
});
}
list() {
return this.prisma.admin.findMany({
async list(query: ListAdminsQuery = {}) {
const page = toPositiveInt(query.page, 1);
const limit = Math.min(toPositiveInt(query.limit, 20), 100);
const keyword = query.keyword?.trim();
const where = {
...(query.role ? { role: query.role as AdminRole } : {}),
...(query.state === 'active' ? { isActive: true } : query.state === 'inactive' ? { isActive: false } : {}),
...(keyword
? {
OR: [
{ email: { contains: keyword, mode: 'insensitive' as const } },
{ name: { contains: keyword, mode: 'insensitive' as const } },
],
}
: {}),
};
const [admins, total] = await this.prisma.$transaction([
this.prisma.admin.findMany({
where,
orderBy: [{ role: 'asc' }, { createdAt: 'desc' }],
skip: (page - 1) * limit,
take: limit,
select: {
id: true,
email: true,
@@ -63,17 +88,44 @@ export class AdminsService {
orderBy: { createdAt: 'desc' },
select: adminSshKeySelect,
},
appPermissions: {
select: { appId: true },
},
});
},
}),
this.prisma.admin.count({ where }),
]);
return {
admins: admins.map((admin, index) => {
return {
...admin,
no: total - ((page - 1) * limit + index),
avatarUrl: getAdminAvatarUrl(admin.id, this.configService),
};
}),
total,
page,
limit,
};
}
async create(body: CreateAdminDto) {
await this.ensureRoleEnabled(body.role);
const email = body.email.trim().toLowerCase();
const name = body.name.trim();
await this.giteaService.createUser({
username: createGiteaUsername(email),
email,
password: body.temporaryPassword,
fullName: name,
});
const admin = await this.prisma.admin.create({
data: {
email: body.email.trim().toLowerCase(),
name: body.name.trim(),
email,
name,
role: body.role,
passwordHash: hashPassword(body.temporaryPassword),
},
@@ -84,7 +136,12 @@ export class AdminsService {
}
async updateStatus(id: string, body: UpdateAdminStatusDto) {
await this.ensureExists(id);
const existing = await this.ensureExists(id);
await this.giteaService.updateUserLoginDisabled(
createGiteaUsername(existing.email),
!body.isActive,
);
const admin = await this.prisma.admin.update({
where: { id },
@@ -132,21 +189,68 @@ export class AdminsService {
return { admin };
}
async listAppPermissions(id: string) {
await this.ensureExists(id);
const permissions = await this.prisma.adminAppPermission.findMany({
where: { adminId: id },
orderBy: { createdAt: 'asc' },
select: { appId: true },
});
return { appIds: permissions.map((permission) => permission.appId) };
}
async updateAppPermissions(id: string, body: UpdateAdminAppPermissionsDto) {
const admin = await this.prisma.admin.findUnique({
where: { id },
select: { id: true, role: true },
});
if (!admin) {
throw new NotFoundException('관리자를 찾을 수 없습니다.');
}
if (admin.role === AdminRole.OWNER) {
throw new BadRequestException('오너 권한은 앱 권한을 개별 설정하지 않습니다.');
}
const appIds = [...new Set(body.appIds.filter(Boolean))];
const existingApps = await this.prisma.app.findMany({
where: { id: { in: appIds } },
select: { id: true },
});
const existingAppIds = new Set(existingApps.map((app) => app.id));
const validAppIds = appIds.filter((appId) => existingAppIds.has(appId));
await this.prisma.$transaction([
this.prisma.adminAppPermission.deleteMany({ where: { adminId: id } }),
...(validAppIds.length
? [
this.prisma.adminAppPermission.createMany({
data: validAppIds.map((appId) => ({ adminId: id, appId })),
skipDuplicates: true,
}),
]
: []),
]);
return { appIds: validAppIds };
}
async createSshKey(adminId: string, body: CreateAdminSshKeyDto) {
await this.ensureExists(adminId);
const admin = await this.ensureExists(adminId);
const parsedKey = parseSshPublicKey(body.publicKey);
const label = body.label?.trim() || `${parsedKey.keyType} ${parsedKey.fingerprint}`;
const giteaUsername = body.giteaUsername?.trim() || null;
const giteaUsername = createGiteaUsername(admin.email);
let giteaKeyId: number | null = null;
if (giteaUsername) {
const giteaKey = await this.giteaService.createUserSshKey(giteaUsername, {
title: label,
key: parsedKey.normalizedKey,
});
giteaKeyId = giteaKey.id;
}
const sshKey = await this.prisma.adminSshKey.create({
data: {
@@ -220,12 +324,14 @@ export class AdminsService {
private async ensureExists(id: string) {
const admin = await this.prisma.admin.findUnique({
where: { id },
select: { id: true },
select: { id: true, email: true },
});
if (!admin) {
throw new NotFoundException('관리자를 찾을 수 없습니다.');
}
return admin;
}
}
@@ -256,6 +362,24 @@ function hashPassword(password: string) {
return `scrypt:${salt}:${hash}`;
}
function toPositiveInt(value: string | number | undefined, fallback: number) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed < 1) return fallback;
return Math.floor(parsed);
}
function createGiteaUsername(email: string) {
const account = email.split('@')[0] || email;
const username = account
.trim()
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, '-')
.replace(/^[._-]+|[._-]+$/g, '')
.slice(0, 40);
return username || `admin-${createHash('sha1').update(email).digest('hex').slice(0, 8)}`;
}
function parseSshPublicKey(publicKey: string) {
const normalizedKey = publicKey.trim().replace(/\s+/g, ' ');
const [keyType, keyBody] = normalizedKey.split(' ');
+31 -1
View File
@@ -1,7 +1,31 @@
import { IsBoolean, IsEmail, IsEnum, IsOptional, IsString, MinLength } from 'class-validator';
import { Transform } from 'class-transformer';
import { IsArray, IsBoolean, IsEmail, IsEnum, IsOptional, IsString, IsUUID, MinLength } from 'class-validator';
import { AdminRole } from './admin-role';
export class ListAdminsQuery {
@IsOptional()
@IsString()
page?: string;
@IsOptional()
@IsString()
limit?: string;
@IsOptional()
@IsString()
keyword?: string;
@IsOptional()
@IsString()
role?: string;
@IsOptional()
@IsString()
state?: string;
}
export class CreateAdminDto {
@Transform(({ value }) => typeof value === 'string' ? value.trim().toLowerCase() : value)
@IsEmail()
email!: string;
@@ -44,3 +68,9 @@ export class CreateAdminSshKeyDto {
@IsString()
giteaUsername?: string;
}
export class UpdateAdminAppPermissionsDto {
@IsArray()
@IsUUID('4', { each: true })
appIds!: string[];
}
+23 -1
View File
@@ -1,7 +1,9 @@
import { Controller, Delete, Get, Param, Patch, Query, Body } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
import {
ListLauncherSshKeysQuery,
CreateDeviceAuthorizationDto,
ListDeviceAuthorizationUsersQuery,
ListProvisionedDevicesQuery,
UpdateProvisionedDeviceDto,
} from './dto';
@@ -38,4 +40,24 @@ export class AgentsController {
removeSshKey(@Param('id') id: string) {
return this.agentsService.removeSshKey(id);
}
@Get('registration-users')
listRegistrationUsers(@Query() query: ListDeviceAuthorizationUsersQuery) {
return this.agentsService.listRegistrationUsers(query);
}
@Get('device-authorizations')
listDeviceAuthorizations() {
return this.agentsService.listDeviceAuthorizations();
}
@Post('device-authorizations')
createDeviceAuthorization(@Body() body: CreateDeviceAuthorizationDto) {
return this.agentsService.createDeviceAuthorization(body.userId);
}
@Post('device-authorizations/:id/cancel')
cancelDeviceAuthorization(@Param('id') id: string) {
return this.agentsService.cancelDeviceAuthorization(id);
}
}
+2 -1
View File
@@ -1,11 +1,12 @@
import { Module } from '@nestjs/common';
import { AuthPrismaModule } from '../auth-prisma/auth-prisma.module';
import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module';
import { AgentsController } from './agents.controller';
import { AgentsService } from './agents.service';
@Module({
imports: [LauncherPrismaModule],
imports: [AuthPrismaModule, LauncherPrismaModule],
controllers: [AgentsController],
providers: [AgentsService],
})
+115 -1
View File
@@ -1,7 +1,10 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { createHash, randomBytes } from 'node:crypto';
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service';
import {
ListDeviceAuthorizationUsersQuery,
ListLauncherSshKeysQuery,
ListProvisionedDevicesQuery,
UpdateProvisionedDeviceDto,
@@ -9,7 +12,10 @@ import {
@Injectable()
export class AgentsService {
constructor(private readonly launcherPrisma: LauncherPrismaService) {}
constructor(
private readonly authPrisma: AuthPrismaService,
private readonly launcherPrisma: LauncherPrismaService,
) {}
async listDevices(query: ListProvisionedDevicesQuery) {
const keyword = query.keyword?.trim();
@@ -109,6 +115,114 @@ export class AgentsService {
return { ok: true };
}
async listRegistrationUsers(query: ListDeviceAuthorizationUsersQuery) {
const keyword = query.keyword?.trim();
const users = await this.authPrisma.user.findMany({
where: {
role: 'USER',
isActive: true,
...(keyword
? {
OR: [
{ email: { contains: keyword, mode: 'insensitive' } },
{ name: { contains: keyword, mode: 'insensitive' } },
],
}
: {}),
},
orderBy: [{ name: 'asc' }, { email: 'asc' }],
take: 20,
select: { id: true, name: true, email: true },
});
return { users };
}
async listDeviceAuthorizations() {
const now = new Date();
const authorizations = await this.launcherPrisma.deviceAuthorization.findMany({
where: {
OR: [
{ status: { in: ['APPROVED', 'CONSUMING'] } },
{ createdAt: { gte: new Date(now.getTime() - 24 * 60 * 60 * 1000) } },
],
},
orderBy: { createdAt: 'desc' },
take: 50,
select: {
id: true,
userCode: true,
status: true,
authUserId: true,
accountEmail: true,
authUserName: true,
expiresAt: true,
createdAt: true,
},
});
return {
authorizations: authorizations.map((authorization) => ({
...authorization,
status: authorization.status === 'APPROVED' && authorization.expiresAt <= now ? 'EXPIRED' : authorization.status,
})),
};
}
async createDeviceAuthorization(userId: string) {
const user = await this.authPrisma.user.findFirst({
where: { id: userId, role: 'USER', isActive: true },
select: { id: true, email: true, name: true, role: true },
});
if (!user) {
throw new NotFoundException('플랫폼 등록 대상 사용자를 찾을 수 없습니다.');
}
const now = new Date();
const expiresAt = new Date(now.getTime() + 5 * 60 * 1000);
await this.launcherPrisma.deviceAuthorization.updateMany({
where: {
authUserId: user.id,
accountEmail: user.email,
status: 'APPROVED',
expiresAt: { gt: now },
},
data: { status: 'CANCELLED' },
});
const authorization = await this.launcherPrisma.deviceAuthorization.create({
data: {
deviceCodeHash: createHash('sha256').update(randomBytes(32).toString('base64url')).digest('hex'),
userCode: String(randomBytes(4).readUInt32BE() % 1_000_000).padStart(6, '0'),
status: 'APPROVED',
authUserId: user.id,
accountEmail: user.email,
authUserName: user.name,
authUserRole: user.role,
authUserRoles: [user.role],
expiresAt,
approvedAt: now,
},
select: { id: true, userCode: true, expiresAt: true, accountEmail: true, authUserName: true, status: true },
});
return { authorization };
}
async cancelDeviceAuthorization(id: string) {
const result = await this.launcherPrisma.deviceAuthorization.updateMany({
where: { id, status: { in: ['APPROVED', 'CONSUMING'] } },
data: { status: 'CANCELLED' },
});
if (!result.count) {
throw new NotFoundException('취소할 플랫폼 등록 키를 찾을 수 없습니다.');
}
return { ok: true };
}
private async ensureDeviceExists(id: string) {
const device = await this.launcherPrisma.provisionedDevice.findUnique({
where: { id },
+12 -1
View File
@@ -1,4 +1,4 @@
import { IsOptional, IsString } from 'class-validator';
import { IsOptional, IsString, IsUUID } from 'class-validator';
export class ListProvisionedDevicesQuery {
@IsOptional()
@@ -37,3 +37,14 @@ export class ListLauncherSshKeysQuery {
@IsString()
keyword?: string;
}
export class ListDeviceAuthorizationUsersQuery {
@IsOptional()
@IsString()
keyword?: string;
}
export class CreateDeviceAuthorizationDto {
@IsUUID()
userId!: string;
}
+12
View File
@@ -11,10 +11,16 @@ import { GiteaModule } from './gitea/gitea.module';
import { HealthModule } from './health/health.module';
import { LauncherModule } from './launcher/launcher.module';
import { LicensesModule } from './licenses/licenses.module';
import { ManualsModule } from './manuals/manuals.module';
import { NoticesModule } from './notices/notices.module';
import { OrganizationsModule } from './organizations/organizations.module';
import { PageAgentModule } from './page-agent/page-agent.module';
import { PrismaModule } from './prisma/prisma.module';
import { RuntimeModule } from './runtime/runtime.module';
import { TailscaleModule } from './tailscale/tailscale.module';
import { TermsModule } from './terms/terms.module';
import { UsersModule } from './users/users.module';
import { WoodpeckerModule } from './woodpecker/woodpecker.module';
@Module({
providers: [
@@ -35,10 +41,16 @@ import { UsersModule } from './users/users.module';
HealthModule,
LauncherModule,
LicensesModule,
ManualsModule,
NoticesModule,
OrganizationsModule,
PageAgentModule,
PrismaModule,
RuntimeModule,
TailscaleModule,
TermsModule,
UsersModule,
WoodpeckerModule,
],
})
export class AppModule {}
+98 -17
View File
@@ -1,4 +1,4 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req } from '@nestjs/common';
import { LinkComponentSourceDto } from '../gitea/dto';
import { AppsService } from './apps.service';
@@ -8,11 +8,13 @@ import {
CreateAppReleaseDto,
CreateBuildJobDto,
CreateDeploymentJobDto,
DecommissionAppDto,
CreateMigrationJobDto,
CreateRuntimeEnvProfileDto,
CreateRuntimeEnvVariableDto,
CreateRuntimeIngressRuleDto,
EnsureRuntimeNamespaceDto,
ReplaceBuildEnvVariablesDto,
} from './dto';
@Controller('apps')
@@ -34,6 +36,14 @@ export class AppsController {
return this.appsService.deleteApp(appId);
}
@Post(':appId/decommission')
decommissionApp(
@Param('appId') appId: string,
@Body() body: DecommissionAppDto,
) {
return this.appsService.decommissionApp(appId, body);
}
@Post(':appId/components')
createComponent(
@Param('appId') appId: string,
@@ -61,13 +71,23 @@ export class AppsController {
}
@Get('builds')
listBuildJobs(@Query('appKey') appKey?: string) {
return this.appsService.listBuildJobs(appKey);
listBuildJobs(
@Query('appKey') appKey?: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
@Query('since') since?: string,
@Query('status') status?: string,
@Query('hideReleaseCandidates') hideReleaseCandidates?: string,
) {
return this.appsService.listBuildJobs(appKey, page, limit, since, status, hideReleaseCandidates);
}
@Post('builds')
createBuildJob(@Body() body: CreateBuildJobDto) {
return this.appsService.createBuildJob(body);
createBuildJob(
@Body() body: CreateBuildJobDto,
@Req() request: { admin?: { id: string; name: string; email: string } },
) {
return this.appsService.createBuildJob(body, request.admin);
}
@Patch('builds/:buildJobId/cancel')
@@ -75,6 +95,11 @@ export class AppsController {
return this.appsService.cancelBuildJob(buildJobId);
}
@Patch('builds/:buildJobId/hide-release-candidate')
hideReleaseCandidate(@Param('buildJobId') buildJobId: string) {
return this.appsService.hideReleaseCandidate(buildJobId);
}
@Delete('builds/:buildJobId')
deleteBuildJob(@Param('buildJobId') buildJobId: string) {
return this.appsService.deleteBuildJob(buildJobId);
@@ -88,34 +113,76 @@ export class AppsController {
return this.appsService.linkComponentSource(componentId, body);
}
@Get('components/:componentId/build-env')
listComponentBuildEnv(@Param('componentId') componentId: string) {
return this.appsService.listComponentBuildEnv(componentId);
}
@Put('components/:componentId/build-env')
replaceComponentBuildEnv(
@Param('componentId') componentId: string,
@Body() body: ReplaceBuildEnvVariablesDto,
) {
return this.appsService.replaceComponentBuildEnv(componentId, body);
}
@Get('releases')
listReleases(@Query('appKey') appKey?: string) {
return this.appsService.listReleases(appKey);
listReleases(
@Query('appKey') appKey?: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
@Query('since') since?: string,
) {
return this.appsService.listReleases(appKey, page, limit, since);
}
@Post('releases')
createRelease(@Body() body: CreateAppReleaseDto) {
return this.appsService.createRelease(body);
createRelease(
@Body() body: CreateAppReleaseDto,
@Req() request: { admin?: { id: string; name: string; email: string } },
) {
return this.appsService.createRelease(body, request.admin);
}
@Delete('releases/:releaseId')
deleteRelease(@Param('releaseId') releaseId: string) {
return this.appsService.deleteRelease(releaseId);
}
@Get('deployments')
listDeployments(@Query('appKey') appKey?: string) {
return this.appsService.listDeployments(appKey);
listDeployments(
@Query('appKey') appKey?: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
@Query('since') since?: string,
) {
return this.appsService.listDeployments(appKey, page, limit, since);
}
@Post('deployments')
createDeploymentJob(@Body() body: CreateDeploymentJobDto) {
return this.appsService.createDeploymentJob(body);
createDeploymentJob(
@Body() body: CreateDeploymentJobDto,
@Req() request: { admin?: { id: string; name: string; email: string } },
) {
return this.appsService.createDeploymentJob(body, request.admin);
}
@Get('migrations')
listMigrationJobs() {
return this.appsService.listMigrationJobs();
listMigrationJobs(
@Query('appId') appId?: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
@Query('since') since?: string,
) {
return this.appsService.listMigrationJobs(appId, page, limit, since);
}
@Post('migrations')
createMigrationJob(@Body() body: CreateMigrationJobDto) {
return this.appsService.createMigrationJob(body);
createMigrationJob(
@Body() body: CreateMigrationJobDto,
@Req() request: { admin?: { id: string; name: string; email: string } },
) {
return this.appsService.createMigrationJob(body, request.admin);
}
@Get('runtime-namespaces/:namespace')
@@ -128,6 +195,15 @@ export class AppsController {
return this.appsService.listRuntimeResources(namespace);
}
@Get('runtime-namespaces/:namespace/resources/:kind/:name/yaml')
getRuntimeResourceYaml(
@Param('namespace') namespace: string,
@Param('kind') kind: string,
@Param('name') name: string,
) {
return this.appsService.getRuntimeResourceYaml(namespace, kind, name);
}
@Post('runtime-namespaces')
ensureRuntimeNamespace(@Body() body: EnsureRuntimeNamespaceDto) {
return this.appsService.ensureRuntimeNamespace(body);
@@ -151,6 +227,11 @@ export class AppsController {
return this.appsService.createRuntimeProfile(body);
}
@Delete('runtime-profiles/:profileId')
deleteRuntimeProfile(@Param('profileId') profileId: string) {
return this.appsService.deleteRuntimeProfile(profileId);
}
@Post('runtime-profiles/:profileId/variables')
createRuntimeVariable(
@Param('profileId') profileId: string,
+1445 -107
View File
File diff suppressed because it is too large Load Diff
+162 -12
View File
@@ -1,9 +1,10 @@
import { spawn, type ChildProcess } from 'node:child_process';
import { execFile, spawn, type ChildProcess } from 'node:child_process';
import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs';
import { promises as fs } from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { promisify } from 'node:util';
import {
Injectable,
@@ -12,14 +13,17 @@ import {
OnModuleInit,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { App, AppComponent, BuildJob } from '@prisma/client';
import { App, AppComponent, BuildEnvVariable, BuildJob } from '@prisma/client';
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { PrismaService } from '../prisma/prisma.service';
const execFileAsync = promisify(execFile);
type BuildJobWithComponent = BuildJob & {
component: AppComponent & {
app: App;
buildVariables: BuildEnvVariable[];
};
};
@@ -93,6 +97,7 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
component: {
include: {
app: true,
buildVariables: true,
},
},
},
@@ -116,6 +121,7 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
component: {
include: {
app: true,
buildVariables: true,
},
},
},
@@ -234,6 +240,29 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
onProcessEnd: () => this.activeProcesses.delete(job.id),
});
}
const resolvedCommitSha = await resolveGitHead(workDir, this.configService.get<string>('GIT_BIN', 'git'));
if (resolvedCommitSha && resolvedCommitSha !== job.commitSha) {
const artifact = getBuildArtifact(job.component, job.branch, resolvedCommitSha);
const image = getBuildImage(
job.component,
job.branch,
resolvedCommitSha,
this.configService.get<string>('BUILD_REGISTRY', 'registry.local'),
);
await this.prisma.buildJob.update({
where: { id: job.id },
data: {
commitSha: resolvedCommitSha,
artifact,
image,
},
});
job.commitSha = resolvedCommitSha;
job.artifact = artifact;
job.image = image;
await append(`[checkout] resolved commit ${resolvedCommitSha}`);
}
}
private async executeByComponentType(
@@ -271,6 +300,25 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
const contextPath = this.configService.get<string>('BUILD_CONTEXT_PATH', '.');
const contextDir = path.resolve(workDir, contextPath);
const pushEnabled = this.configService.get<string>('BUILD_PUSH_ENABLED', 'true') !== 'false';
const viteBuildVariables = job.component.buildVariables
.filter((variable) => !variable.isSecret && variable.key.trim().startsWith('VITE_'));
const buildArgs = job.component.buildVariables
.filter((variable) => !variable.isSecret && variable.key.trim())
.filter((variable) => !variable.key.trim().startsWith('VITE_'))
.flatMap((variable) => ['--build-arg', `${variable.key}=${variable.value}`]);
if (viteBuildVariables.length > 0) {
await fs.writeFile(
path.join(contextDir, '.env.production.local'),
`${viteBuildVariables.map((variable) => `${variable.key}=${JSON.stringify(variable.value)}`).join('\n')}\n`,
'utf8',
);
await append(`[image] Vite build env ${viteBuildVariables.map((variable) => variable.key).join(', ')}`);
}
if (buildArgs.length > 0) {
await append(`[image] build args ${job.component.buildVariables.filter((variable) => !variable.isSecret && variable.key.trim() && !variable.key.trim().startsWith('VITE_')).map((variable) => variable.key).join(', ')}`);
}
if (image !== job.image) {
await this.prisma.buildJob.update({
@@ -287,7 +335,7 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
await runCommand({
jobId: job.id,
command: this.configService.get<string>('NERDCTL_BIN', 'nerdctl'),
args: [...prefix, 'build', '-t', image, '-f', dockerfile, '.'],
args: [...prefix, 'build', ...buildArgs, '-t', image, '-f', dockerfile, '.'],
cwd: contextDir,
append,
onProcess: (child) => this.activeProcesses.set(job.id, child),
@@ -311,7 +359,7 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
await runCommand({
jobId: job.id,
command: this.configService.get<string>('DOCKER_BIN', 'docker'),
args: ['build', '-t', image, '-f', dockerfile, '.'],
args: ['build', ...buildArgs, '-t', image, '-f', dockerfile, '.'],
cwd: contextDir,
append,
onProcess: (child) => this.activeProcesses.set(job.id, child),
@@ -360,12 +408,18 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
'BUILD_ARTIFACT_DIR',
path.join(os.tmpdir(), 'studio-admin-artifacts'),
);
const buildEnv = resolveBuildEnv(job.component.buildVariables);
if (job.component.buildVariables.length > 0) {
await append(`[artifact] build env ${job.component.buildVariables.map((variable) => `${variable.key}=${variable.isSecret ? '********' : variable.value}`).join(' ')}`);
}
await append(`[artifact] install ${installCommand}`);
await runShellCommand({
jobId: job.id,
command: installCommand,
cwd: workDir,
env: buildEnv,
append,
onProcess: (child) => this.activeProcesses.set(job.id, child),
onProcessEnd: () => this.activeProcesses.delete(job.id),
@@ -376,6 +430,7 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
jobId: job.id,
command: buildCommand,
cwd: workDir,
env: buildEnv,
append,
onProcess: (child) => this.activeProcesses.set(job.id, child),
onProcessEnd: () => this.activeProcesses.delete(job.id),
@@ -385,16 +440,20 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
if (!artifact) {
throw new Error(`산출물을 찾을 수 없습니다: ${outputDir}`);
}
const artifactVersion = await readPackageVersion(workDir);
await fs.mkdir(artifactDir, { recursive: true });
const destinationName = job.artifact || path.basename(artifact);
const destination = path.join(artifactDir, destinationName);
await fs.copyFile(artifact, destination);
const artifactSha256 = await sha256File(artifact);
const uploadedArtifact = await this.uploadArtifactIfConfigured(
job,
destination,
destinationName,
artifactVersion,
artifactSha256,
append,
);
const publicBaseUrl = this.configService.get<string>('BUILD_ARTIFACT_BASE_URL');
@@ -405,9 +464,10 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
await this.prisma.buildJob.update({
where: { id: job.id },
data: { artifact: artifactValue },
data: { artifact: artifactValue, artifactSha256 },
});
job.artifact = artifactValue;
job.artifactSha256 = artifactSha256;
await append(`[artifact] found ${path.relative(workDir, artifact)}`);
await append(`[artifact] copied to ${artifactValue}`);
@@ -417,6 +477,8 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
job: BuildJobWithComponent,
artifactPath: string,
artifactName: string,
artifactVersion: string | null,
sha256: string,
append: (line: string) => Promise<void>,
) {
const bucket = this.configService.get<string>('R2_BUCKET');
@@ -428,9 +490,8 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
return null;
}
const key = this.createArtifactObjectKey(job, artifactName);
const key = this.createArtifactObjectKey(job, artifactName, artifactVersion);
const stat = await fs.stat(artifactPath);
const sha256 = await sha256File(artifactPath);
const client = this.getR2Client(accessKeyId, secretAccessKey);
await append(`[r2] upload ${key}`);
@@ -447,6 +508,7 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
component: safeMetadataValue(job.component.name),
branch: safeMetadataValue(job.branch),
commit: safeMetadataValue(job.commitSha ?? ''),
version: safeMetadataValue(artifactVersion ?? ''),
},
}));
@@ -483,16 +545,21 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
return this.r2Client;
}
private createArtifactObjectKey(job: BuildJobWithComponent, artifactName: string) {
private createArtifactObjectKey(
job: BuildJobWithComponent,
artifactName: string,
artifactVersion: string | null,
) {
const prefix = this.configService.get<string>('R2_ARTIFACT_PREFIX', 'studio-artifacts');
const revision = job.commitSha?.trim().slice(0, 7) || job.id;
const versionPath = artifactVersion
? [artifactVersion]
: [job.branch, 'latest'];
return [
prefix,
'apps',
job.component.app.key,
job.component.name,
job.branch,
revision,
...versionPath,
artifactName,
].map(sanitizeObjectKeyPart).join('/');
}
@@ -548,6 +615,64 @@ export class BuildRunnerService implements OnModuleInit, OnModuleDestroy {
}
}
async function resolveGitHead(workDir: string, gitBin = 'git') {
const { stdout } = await execFileAsync(gitBin, ['rev-parse', 'HEAD'], { cwd: workDir });
return stdout.trim() || null;
}
function getBuildArtifact(
component: { name: string; type: string },
branch: string,
commitSha: string | null,
) {
const suffix = getBuildSuffix(branch, commitSha);
if (component.type === 'ELECTRON') {
return `${component.name}-${suffix}.AppImage`;
}
if (component.type === 'MIGRATION') {
return `${component.name}:${branch}:migration-check`;
}
return null;
}
function getBuildImage(
component: { name: string; type: string },
branch: string,
commitSha: string | null,
registry: string,
) {
if (component.type === 'ELECTRON' || component.type === 'MIGRATION') {
return null;
}
return `${registry.replace(/\/$/, '')}/${sanitizeImageName(
component.name,
)}:${sanitizeImageTag(getBuildSuffix(branch, commitSha))}`;
}
function getBuildSuffix(branch: string, commitSha: string | null) {
return commitSha ? commitSha.slice(0, 7) : branch;
}
function sanitizeImageName(value: string) {
return value
.trim()
.toLowerCase()
.replace(/[^a-z0-9._/-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
function sanitizeImageTag(value: string) {
return value
.trim()
.replace(/[^A-Za-z0-9_.-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
async function runCommand({
jobId,
command,
@@ -616,6 +741,7 @@ async function runShellCommand({
jobId,
command,
cwd,
env,
append,
onProcess,
onProcessEnd,
@@ -623,6 +749,7 @@ async function runShellCommand({
jobId: string;
command: string;
cwd: string;
env?: NodeJS.ProcessEnv;
append: (line: string) => Promise<void>;
onProcess: (child: ChildProcess) => void;
onProcessEnd: () => void;
@@ -632,7 +759,7 @@ async function runShellCommand({
await new Promise<void>((resolve, reject) => {
const child = spawn(command, {
cwd,
env: process.env,
env: env ?? process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: true,
shell: true,
@@ -673,12 +800,35 @@ async function runShellCommand({
});
}
function resolveBuildEnv(variables: BuildEnvVariable[]): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = { ...process.env };
for (const variable of variables) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(variable.key)) continue;
env[variable.key] = variable.value;
}
return env;
}
async function findNewestArtifact(dir: string) {
const candidates = await findArtifactCandidates(dir).catch(() => []);
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
return candidates[0]?.path ?? null;
}
async function readPackageVersion(workDir: string) {
try {
const packageJson = await fs.readFile(path.join(workDir, 'package.json'), 'utf8');
const parsed = JSON.parse(packageJson) as { version?: unknown };
return typeof parsed.version === 'string' && parsed.version.trim()
? parsed.version.trim()
: null;
} catch {
return null;
}
}
async function findArtifactCandidates(dir: string): Promise<Array<{ path: string; mtimeMs: number }>> {
const entries = await fs.readdir(dir, { withFileTypes: true });
const results: Array<{ path: string; mtimeMs: number }> = [];
+80 -2
View File
@@ -1,4 +1,5 @@
import { ArrayNotEmpty, IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, IsUUID, Min } from 'class-validator';
import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, IsUUID, Min, ValidateNested } from 'class-validator';
import { AppStatus, ComponentType } from './app-enums';
export class CreateAppDto {
@@ -21,6 +22,28 @@ export class CreateAppDto {
status?: AppStatus;
}
export class DecommissionAppDto {
@IsOptional()
@IsBoolean()
unlinkSources?: boolean;
@IsOptional()
@IsBoolean()
deleteRuntimeSettings?: boolean;
@IsOptional()
@IsBoolean()
scaleDownWorkloads?: boolean;
@IsOptional()
@IsBoolean()
deleteKubernetesResources?: boolean;
@IsOptional()
@IsBoolean()
deletePersistentVolumes?: boolean;
}
export class CreateAppComponentDto {
@IsString()
name!: string;
@@ -93,6 +116,7 @@ export class CreateAppComponentDto {
name?: string;
port?: number;
targetPort?: number;
nodePort?: number;
protocol?: string;
}>;
}
@@ -112,6 +136,10 @@ export class CreateRuntimeIngressRuleDto {
@Min(1)
servicePort!: number;
@IsOptional()
@IsBoolean()
stripPrefix?: boolean;
@IsOptional()
@IsBoolean()
tlsEnabled?: boolean;
@@ -134,6 +162,23 @@ export class CreateBuildJobDto {
commitSha?: string;
}
export class BuildEnvVariableInputDto {
@IsString()
key!: string;
@IsString()
value!: string;
@IsOptional()
@IsBoolean()
isSecret?: boolean;
}
export class ReplaceBuildEnvVariablesDto {
@IsArray()
variables!: BuildEnvVariableInputDto[];
}
export class CreateAppReleaseDto {
@IsUUID()
appId!: string;
@@ -151,9 +196,37 @@ export class CreateAppReleaseDto {
buildJobIds!: string[];
}
export class DeploymentResourceLimitsDto {
@IsOptional()
@Type(() => Number)
@IsInt()
replicas?: number;
@IsOptional()
@IsString()
cpuRequest?: string;
@IsOptional()
@IsString()
cpuLimit?: string;
@IsOptional()
@IsString()
memoryRequest?: string;
@IsOptional()
@IsString()
memoryLimit?: string;
}
export class CreateDeploymentJobDto {
@IsOptional()
@IsUUID()
releaseId!: string;
releaseId?: string;
@IsOptional()
@IsUUID()
buildJobId?: string;
@IsString()
environment!: string;
@@ -164,6 +237,11 @@ export class CreateDeploymentJobDto {
@IsOptional()
@IsBoolean()
updateLauncherAppVersion?: boolean;
@IsOptional()
@ValidateNested()
@Type(() => DeploymentResourceLimitsDto)
resources?: DeploymentResourceLimitsDto;
}
export class CreateRuntimeEnvProfileDto {
-1
View File
@@ -1 +0,0 @@
export * from "./index"
-5
View File
@@ -1,5 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('.') }
-1
View File
@@ -1 +0,0 @@
export * from "./index"
-5
View File
@@ -1,5 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('#main-entry-point') }
-1
View File
@@ -1 +0,0 @@
export * from "./default"
File diff suppressed because one or more lines are too long
-217
View File
@@ -1,217 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
objectEnumValues,
makeStrictEnum,
Public,
getRuntime,
skip
} = require('./runtime/index-browser.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.19.3
* Query Engine version: c2990dca591cba766e3b7ef5d9e8a84796e47ab7
*/
Prisma.prismaVersion = {
client: "6.19.3",
engine: "c2990dca591cba766e3b7ef5d9e8a84796e47ab7"
}
Prisma.PrismaClientKnownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)};
Prisma.PrismaClientUnknownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientRustPanicError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientInitializationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientValidationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.empty = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.join = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.raw = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.defineExtension = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull
Prisma.NullTypes = {
DbNull: objectEnumValues.classes.DbNull,
JsonNull: objectEnumValues.classes.JsonNull,
AnyNull: objectEnumValues.classes.AnyNull
}
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
exports.Prisma.UserScalarFieldEnum = {
id: 'id',
email: 'email',
passwordHash: 'passwordHash',
name: 'name',
role: 'role',
isActive: 'isActive',
lastLoginAt: 'lastLoginAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.LicenseScalarFieldEnum = {
id: 'id',
code: 'code',
name: 'name',
description: 'description',
defaultDurationDays: 'defaultDurationDays',
isActive: 'isActive',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.UserLicenseScalarFieldEnum = {
id: 'id',
userId: 'userId',
licenseId: 'licenseId',
startsAt: 'startsAt',
expiresAt: 'expiresAt',
assignedById: 'assignedById',
createdAt: 'createdAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.UserRole = exports.$Enums.UserRole = {
ADMIN: 'ADMIN',
USER: 'USER'
};
exports.LicenseCode = exports.$Enums.LicenseCode = {
FREE: 'FREE',
EDUCATION: 'EDUCATION',
PROFESSIONAL: 'PROFESSIONAL'
};
exports.Prisma.ModelName = {
User: 'User',
License: 'License',
UserLicense: 'UserLicense'
};
/**
* This is a stub Prisma Client that will error at runtime if called.
*/
class PrismaClient {
constructor() {
return new Proxy(this, {
get(target, prop) {
let message
const runtime = getRuntime()
if (runtime.isEdge) {
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
- Use Prisma Accelerate: https://pris.ly/d/accelerate
- Use Driver Adapters: https://pris.ly/d/driver-adapters
`;
} else {
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
}
message += `
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
throw new Error(message)
}
})
}
}
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-183
View File
@@ -1,183 +0,0 @@
{
"name": "prisma-client-378612c311cf1c6fac598c21e6ea51068a0548bfbc7fdc08b65098bb7a2e5523",
"main": "index.js",
"types": "index.d.ts",
"browser": "default.js",
"exports": {
"./client": {
"require": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./package.json": "./package.json",
".": {
"require": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
},
"./react-native": {
"types": "./react-native.d.ts",
"require": "./react-native.js",
"import": "./react-native.js",
"default": "./react-native.js"
},
"./extension": {
"types": "./extension.d.ts",
"require": "./extension.js",
"import": "./extension.js",
"default": "./extension.js"
},
"./index-browser": {
"types": "./index.d.ts",
"require": "./index-browser.js",
"import": "./index-browser.js",
"default": "./index-browser.js"
},
"./index": {
"types": "./index.d.ts",
"require": "./index.js",
"import": "./index.js",
"default": "./index.js"
},
"./wasm": {
"types": "./wasm.d.ts",
"require": "./wasm.js",
"import": "./wasm.mjs",
"default": "./wasm.mjs"
},
"./runtime/client": {
"types": "./runtime/client.d.ts",
"node": {
"require": "./runtime/client.js",
"default": "./runtime/client.js"
},
"require": "./runtime/client.js",
"import": "./runtime/client.mjs",
"default": "./runtime/client.mjs"
},
"./runtime/library": {
"types": "./runtime/library.d.ts",
"require": "./runtime/library.js",
"import": "./runtime/library.mjs",
"default": "./runtime/library.mjs"
},
"./runtime/binary": {
"types": "./runtime/binary.d.ts",
"require": "./runtime/binary.js",
"import": "./runtime/binary.mjs",
"default": "./runtime/binary.mjs"
},
"./runtime/wasm-engine-edge": {
"types": "./runtime/wasm-engine-edge.d.ts",
"require": "./runtime/wasm-engine-edge.js",
"import": "./runtime/wasm-engine-edge.mjs",
"default": "./runtime/wasm-engine-edge.mjs"
},
"./runtime/wasm-compiler-edge": {
"types": "./runtime/wasm-compiler-edge.d.ts",
"require": "./runtime/wasm-compiler-edge.js",
"import": "./runtime/wasm-compiler-edge.mjs",
"default": "./runtime/wasm-compiler-edge.mjs"
},
"./runtime/edge": {
"types": "./runtime/edge.d.ts",
"require": "./runtime/edge.js",
"import": "./runtime/edge-esm.js",
"default": "./runtime/edge-esm.js"
},
"./runtime/react-native": {
"types": "./runtime/react-native.d.ts",
"require": "./runtime/react-native.js",
"import": "./runtime/react-native.js",
"default": "./runtime/react-native.js"
},
"./runtime/index-browser": {
"types": "./runtime/index-browser.d.ts",
"require": "./runtime/index-browser.js",
"import": "./runtime/index-browser.mjs",
"default": "./runtime/index-browser.mjs"
},
"./generator-build": {
"require": "./generator-build/index.js",
"import": "./generator-build/index.js",
"default": "./generator-build/index.js"
},
"./sql": {
"require": {
"types": "./sql.d.ts",
"node": "./sql.js",
"default": "./sql.js"
},
"import": {
"types": "./sql.d.ts",
"node": "./sql.mjs",
"default": "./sql.mjs"
},
"default": "./sql.js"
},
"./*": "./*"
},
"version": "6.19.3",
"sideEffects": false,
"imports": {
"#wasm-engine-loader": {
"edge-light": "./wasm-edge-light-loader.mjs",
"workerd": "./wasm-worker-loader.mjs",
"worker": "./wasm-worker-loader.mjs",
"default": "./wasm-worker-loader.mjs"
},
"#main-entry-point": {
"require": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
}
}
}
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-370
View File
@@ -1,370 +0,0 @@
declare class AnyNull extends NullTypesEnumValue {
#private;
}
declare type Args<T, F extends Operation> = T extends {
[K: symbol]: {
types: {
operations: {
[K in F]: {
args: any;
};
};
};
};
} ? T[symbol]['types']['operations'][F]['args'] : any;
declare class DbNull extends NullTypesEnumValue {
#private;
}
export declare function Decimal(n: Decimal.Value): Decimal;
export declare namespace Decimal {
export type Constructor = typeof Decimal;
export type Instance = Decimal;
export type Rounding = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export type Modulo = Rounding | 9;
export type Value = string | number | Decimal;
// http://mikemcl.github.io/decimal.js/#constructor-properties
export interface Config {
precision?: number;
rounding?: Rounding;
toExpNeg?: number;
toExpPos?: number;
minE?: number;
maxE?: number;
crypto?: boolean;
modulo?: Modulo;
defaults?: boolean;
}
}
export declare class Decimal {
readonly d: number[];
readonly e: number;
readonly s: number;
constructor(n: Decimal.Value);
absoluteValue(): Decimal;
abs(): Decimal;
ceil(): Decimal;
clampedTo(min: Decimal.Value, max: Decimal.Value): Decimal;
clamp(min: Decimal.Value, max: Decimal.Value): Decimal;
comparedTo(n: Decimal.Value): number;
cmp(n: Decimal.Value): number;
cosine(): Decimal;
cos(): Decimal;
cubeRoot(): Decimal;
cbrt(): Decimal;
decimalPlaces(): number;
dp(): number;
dividedBy(n: Decimal.Value): Decimal;
div(n: Decimal.Value): Decimal;
dividedToIntegerBy(n: Decimal.Value): Decimal;
divToInt(n: Decimal.Value): Decimal;
equals(n: Decimal.Value): boolean;
eq(n: Decimal.Value): boolean;
floor(): Decimal;
greaterThan(n: Decimal.Value): boolean;
gt(n: Decimal.Value): boolean;
greaterThanOrEqualTo(n: Decimal.Value): boolean;
gte(n: Decimal.Value): boolean;
hyperbolicCosine(): Decimal;
cosh(): Decimal;
hyperbolicSine(): Decimal;
sinh(): Decimal;
hyperbolicTangent(): Decimal;
tanh(): Decimal;
inverseCosine(): Decimal;
acos(): Decimal;
inverseHyperbolicCosine(): Decimal;
acosh(): Decimal;
inverseHyperbolicSine(): Decimal;
asinh(): Decimal;
inverseHyperbolicTangent(): Decimal;
atanh(): Decimal;
inverseSine(): Decimal;
asin(): Decimal;
inverseTangent(): Decimal;
atan(): Decimal;
isFinite(): boolean;
isInteger(): boolean;
isInt(): boolean;
isNaN(): boolean;
isNegative(): boolean;
isNeg(): boolean;
isPositive(): boolean;
isPos(): boolean;
isZero(): boolean;
lessThan(n: Decimal.Value): boolean;
lt(n: Decimal.Value): boolean;
lessThanOrEqualTo(n: Decimal.Value): boolean;
lte(n: Decimal.Value): boolean;
logarithm(n?: Decimal.Value): Decimal;
log(n?: Decimal.Value): Decimal;
minus(n: Decimal.Value): Decimal;
sub(n: Decimal.Value): Decimal;
modulo(n: Decimal.Value): Decimal;
mod(n: Decimal.Value): Decimal;
naturalExponential(): Decimal;
exp(): Decimal;
naturalLogarithm(): Decimal;
ln(): Decimal;
negated(): Decimal;
neg(): Decimal;
plus(n: Decimal.Value): Decimal;
add(n: Decimal.Value): Decimal;
precision(includeZeros?: boolean): number;
sd(includeZeros?: boolean): number;
round(): Decimal;
sine() : Decimal;
sin() : Decimal;
squareRoot(): Decimal;
sqrt(): Decimal;
tangent() : Decimal;
tan() : Decimal;
times(n: Decimal.Value): Decimal;
mul(n: Decimal.Value) : Decimal;
toBinary(significantDigits?: number): string;
toBinary(significantDigits: number, rounding: Decimal.Rounding): string;
toDecimalPlaces(decimalPlaces?: number): Decimal;
toDecimalPlaces(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
toDP(decimalPlaces?: number): Decimal;
toDP(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
toExponential(decimalPlaces?: number): string;
toExponential(decimalPlaces: number, rounding: Decimal.Rounding): string;
toFixed(decimalPlaces?: number): string;
toFixed(decimalPlaces: number, rounding: Decimal.Rounding): string;
toFraction(max_denominator?: Decimal.Value): Decimal[];
toHexadecimal(significantDigits?: number): string;
toHexadecimal(significantDigits: number, rounding: Decimal.Rounding): string;
toHex(significantDigits?: number): string;
toHex(significantDigits: number, rounding?: Decimal.Rounding): string;
toJSON(): string;
toNearest(n: Decimal.Value, rounding?: Decimal.Rounding): Decimal;
toNumber(): number;
toOctal(significantDigits?: number): string;
toOctal(significantDigits: number, rounding: Decimal.Rounding): string;
toPower(n: Decimal.Value): Decimal;
pow(n: Decimal.Value): Decimal;
toPrecision(significantDigits?: number): string;
toPrecision(significantDigits: number, rounding: Decimal.Rounding): string;
toSignificantDigits(significantDigits?: number): Decimal;
toSignificantDigits(significantDigits: number, rounding: Decimal.Rounding): Decimal;
toSD(significantDigits?: number): Decimal;
toSD(significantDigits: number, rounding: Decimal.Rounding): Decimal;
toString(): string;
truncated(): Decimal;
trunc(): Decimal;
valueOf(): string;
static abs(n: Decimal.Value): Decimal;
static acos(n: Decimal.Value): Decimal;
static acosh(n: Decimal.Value): Decimal;
static add(x: Decimal.Value, y: Decimal.Value): Decimal;
static asin(n: Decimal.Value): Decimal;
static asinh(n: Decimal.Value): Decimal;
static atan(n: Decimal.Value): Decimal;
static atanh(n: Decimal.Value): Decimal;
static atan2(y: Decimal.Value, x: Decimal.Value): Decimal;
static cbrt(n: Decimal.Value): Decimal;
static ceil(n: Decimal.Value): Decimal;
static clamp(n: Decimal.Value, min: Decimal.Value, max: Decimal.Value): Decimal;
static clone(object?: Decimal.Config): Decimal.Constructor;
static config(object: Decimal.Config): Decimal.Constructor;
static cos(n: Decimal.Value): Decimal;
static cosh(n: Decimal.Value): Decimal;
static div(x: Decimal.Value, y: Decimal.Value): Decimal;
static exp(n: Decimal.Value): Decimal;
static floor(n: Decimal.Value): Decimal;
static hypot(...n: Decimal.Value[]): Decimal;
static isDecimal(object: any): object is Decimal;
static ln(n: Decimal.Value): Decimal;
static log(n: Decimal.Value, base?: Decimal.Value): Decimal;
static log2(n: Decimal.Value): Decimal;
static log10(n: Decimal.Value): Decimal;
static max(...n: Decimal.Value[]): Decimal;
static min(...n: Decimal.Value[]): Decimal;
static mod(x: Decimal.Value, y: Decimal.Value): Decimal;
static mul(x: Decimal.Value, y: Decimal.Value): Decimal;
static noConflict(): Decimal.Constructor; // Browser only
static pow(base: Decimal.Value, exponent: Decimal.Value): Decimal;
static random(significantDigits?: number): Decimal;
static round(n: Decimal.Value): Decimal;
static set(object: Decimal.Config): Decimal.Constructor;
static sign(n: Decimal.Value): number;
static sin(n: Decimal.Value): Decimal;
static sinh(n: Decimal.Value): Decimal;
static sqrt(n: Decimal.Value): Decimal;
static sub(x: Decimal.Value, y: Decimal.Value): Decimal;
static sum(...n: Decimal.Value[]): Decimal;
static tan(n: Decimal.Value): Decimal;
static tanh(n: Decimal.Value): Decimal;
static trunc(n: Decimal.Value): Decimal;
static readonly default?: Decimal.Constructor;
static readonly Decimal?: Decimal.Constructor;
static readonly precision: number;
static readonly rounding: Decimal.Rounding;
static readonly toExpNeg: number;
static readonly toExpPos: number;
static readonly minE: number;
static readonly maxE: number;
static readonly crypto: boolean;
static readonly modulo: Decimal.Modulo;
static readonly ROUND_UP: 0;
static readonly ROUND_DOWN: 1;
static readonly ROUND_CEIL: 2;
static readonly ROUND_FLOOR: 3;
static readonly ROUND_HALF_UP: 4;
static readonly ROUND_HALF_DOWN: 5;
static readonly ROUND_HALF_EVEN: 6;
static readonly ROUND_HALF_CEIL: 7;
static readonly ROUND_HALF_FLOOR: 8;
static readonly EUCLID: 9;
}
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
[K in keyof A]: Exact<A[K], W[K]>;
} : W) : never) | (A extends Narrowable ? A : never);
export declare function getRuntime(): GetRuntimeOutput;
declare type GetRuntimeOutput = {
id: RuntimeName;
prettyName: string;
isEdge: boolean;
};
declare class JsonNull extends NullTypesEnumValue {
#private;
}
/**
* Generates more strict variant of an enum which, unlike regular enum,
* throws on non-existing property access. This can be useful in following situations:
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
* - enum values are generated dynamically from DMMF.
*
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
* will result in `undefined` value being used, which will be accepted. Using strict enum
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
*
* Note: if you need to check for existence of a value in the enum you can still use either
* `in` operator or `hasOwnProperty` function.
*
* @param definition
* @returns
*/
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
declare type Narrowable = string | number | bigint | boolean | [];
declare class NullTypesEnumValue extends ObjectEnumValue {
_getNamespace(): string;
}
/**
* Base class for unique values of object-valued enums.
*/
declare abstract class ObjectEnumValue {
constructor(arg?: symbol);
abstract _getNamespace(): string;
_getName(): string;
toString(): string;
}
export declare const objectEnumValues: {
classes: {
DbNull: typeof DbNull;
JsonNull: typeof JsonNull;
AnyNull: typeof AnyNull;
};
instances: {
DbNull: DbNull;
JsonNull: JsonNull;
AnyNull: AnyNull;
};
};
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'updateManyAndReturn' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';
declare namespace Public {
export {
validator
}
}
export { Public }
declare type RuntimeName = 'workerd' | 'deno' | 'netlify' | 'node' | 'bun' | 'edge-light' | '';
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation, P extends keyof Args<C[M], O>>(client: C, model: M, operation: O, prop: P): <S>(select: Exact<S, Args<C[M], O>[P]>) => S;
export { }
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-66
View File
@@ -1,66 +0,0 @@
generator authClient {
provider = "prisma-client-js"
output = "../src/generated/auth-client"
}
datasource db {
provider = "postgresql"
url = env("AUTH_DATABASE_URL")
}
enum UserRole {
ADMIN
USER
}
enum LicenseCode {
FREE
EDUCATION
PROFESSIONAL
}
model User {
id String @id @default(uuid()) @db.Uuid
email String @unique
passwordHash String @map("password_hash")
name String?
role UserRole @default(USER)
isActive Boolean @default(true) @map("is_active")
lastLoginAt DateTime? @map("last_login_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
userLicenses UserLicense[]
@@map("users")
}
model License {
id String @id @default(uuid()) @db.Uuid
code LicenseCode @unique
name String
description String?
defaultDurationDays Int? @default(365) @map("default_duration_days")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
userLicenses UserLicense[]
@@index([isActive])
@@map("licenses")
}
model UserLicense {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
licenseId String @map("license_id") @db.Uuid
startsAt DateTime @default(now()) @map("starts_at") @db.Timestamptz(3)
expiresAt DateTime? @map("expires_at") @db.Timestamptz(3)
assignedById String? @map("assigned_by") @db.Uuid
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
license License @relation(fields: [licenseId], references: [id], onDelete: Restrict)
@@index([userId, startsAt])
@@index([licenseId])
@@map("user_licenses")
}
@@ -1,5 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_engine_bg.wasm?module')
@@ -1,5 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_engine_bg.wasm')
-1
View File
@@ -1 +0,0 @@
export * from "./default"
-238
View File
@@ -1,238 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
Object.defineProperty(exports, "__esModule", { value: true });
const {
PrismaClientKnownRequestError,
PrismaClientUnknownRequestError,
PrismaClientRustPanicError,
PrismaClientInitializationError,
PrismaClientValidationError,
getPrismaClient,
sqltag,
empty,
join,
raw,
skip,
Decimal,
Debug,
objectEnumValues,
makeStrictEnum,
Extensions,
warnOnce,
defineDmmfProperty,
Public,
getRuntime,
createParam,
} = require('./runtime/wasm-engine-edge.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.19.3
* Query Engine version: c2990dca591cba766e3b7ef5d9e8a84796e47ab7
*/
Prisma.prismaVersion = {
client: "6.19.3",
engine: "c2990dca591cba766e3b7ef5d9e8a84796e47ab7"
}
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
Prisma.PrismaClientUnknownRequestError = PrismaClientUnknownRequestError
Prisma.PrismaClientRustPanicError = PrismaClientRustPanicError
Prisma.PrismaClientInitializationError = PrismaClientInitializationError
Prisma.PrismaClientValidationError = PrismaClientValidationError
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = sqltag
Prisma.empty = empty
Prisma.join = join
Prisma.raw = raw
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = Extensions.getExtensionContext
Prisma.defineExtension = Extensions.defineExtension
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull
Prisma.NullTypes = {
DbNull: objectEnumValues.classes.DbNull,
JsonNull: objectEnumValues.classes.JsonNull,
AnyNull: objectEnumValues.classes.AnyNull
}
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
exports.Prisma.UserScalarFieldEnum = {
id: 'id',
email: 'email',
passwordHash: 'passwordHash',
name: 'name',
role: 'role',
isActive: 'isActive',
lastLoginAt: 'lastLoginAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.LicenseScalarFieldEnum = {
id: 'id',
code: 'code',
name: 'name',
description: 'description',
defaultDurationDays: 'defaultDurationDays',
isActive: 'isActive',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.UserLicenseScalarFieldEnum = {
id: 'id',
userId: 'userId',
licenseId: 'licenseId',
startsAt: 'startsAt',
expiresAt: 'expiresAt',
assignedById: 'assignedById',
createdAt: 'createdAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.UserRole = exports.$Enums.UserRole = {
ADMIN: 'ADMIN',
USER: 'USER'
};
exports.LicenseCode = exports.$Enums.LicenseCode = {
FREE: 'FREE',
EDUCATION: 'EDUCATION',
PROFESSIONAL: 'PROFESSIONAL'
};
exports.Prisma.ModelName = {
User: 'User',
License: 'License',
UserLicense: 'UserLicense'
};
/**
* Create the Client
*/
const config = {
"generator": {
"name": "authClient",
"provider": {
"fromEnvVar": null,
"value": "prisma-client-js"
},
"output": {
"value": "/home/chyoo/tmp/atnmy-stdo-v2/atnmy-stdo-adm-v2-be/src/generated/auth-client",
"fromEnvVar": null
},
"config": {
"engineType": "library"
},
"binaryTargets": [
{
"fromEnvVar": null,
"value": "debian-openssl-3.0.x",
"native": true
}
],
"previewFeatures": [],
"sourceFilePath": "/home/chyoo/tmp/atnmy-stdo-v2/atnmy-stdo-adm-v2-be/prisma/auth.schema.prisma",
"isCustomOutput": true
},
"relativeEnvPaths": {
"rootEnvPath": null,
"schemaEnvPath": "../../../.env"
},
"relativePath": "../../../prisma",
"clientVersion": "6.19.3",
"engineVersion": "c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
"datasourceNames": [
"db"
],
"activeProvider": "postgresql",
"postinstall": false,
"inlineDatasources": {
"db": {
"url": {
"fromEnvVar": "AUTH_DATABASE_URL",
"value": null
}
}
},
"inlineSchema": "generator authClient {\n provider = \"prisma-client-js\"\n output = \"../src/generated/auth-client\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"AUTH_DATABASE_URL\")\n}\n\nenum UserRole {\n ADMIN\n USER\n}\n\nenum LicenseCode {\n FREE\n EDUCATION\n PROFESSIONAL\n}\n\nmodel User {\n id String @id @default(uuid()) @db.Uuid\n email String @unique\n passwordHash String @map(\"password_hash\")\n name String?\n role UserRole @default(USER)\n isActive Boolean @default(true) @map(\"is_active\")\n lastLoginAt DateTime? @map(\"last_login_at\") @db.Timestamptz(3)\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n userLicenses UserLicense[]\n\n @@map(\"users\")\n}\n\nmodel License {\n id String @id @default(uuid()) @db.Uuid\n code LicenseCode @unique\n name String\n description String?\n defaultDurationDays Int? @default(365) @map(\"default_duration_days\")\n isActive Boolean @default(true) @map(\"is_active\")\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n updatedAt DateTime @updatedAt @map(\"updated_at\") @db.Timestamptz(3)\n userLicenses UserLicense[]\n\n @@index([isActive])\n @@map(\"licenses\")\n}\n\nmodel UserLicense {\n id String @id @default(uuid()) @db.Uuid\n userId String @map(\"user_id\") @db.Uuid\n licenseId String @map(\"license_id\") @db.Uuid\n startsAt DateTime @default(now()) @map(\"starts_at\") @db.Timestamptz(3)\n expiresAt DateTime? @map(\"expires_at\") @db.Timestamptz(3)\n assignedById String? @map(\"assigned_by\") @db.Uuid\n createdAt DateTime @default(now()) @map(\"created_at\") @db.Timestamptz(3)\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n license License @relation(fields: [licenseId], references: [id], onDelete: Restrict)\n\n @@index([userId, startsAt])\n @@index([licenseId])\n @@map(\"user_licenses\")\n}\n",
"inlineSchemaHash": "b41ead22c04f404adbe239bd44c2751b9ed9adf3c5b7b7013be05252086f79d7",
"copyEngine": true
}
config.dirname = '/'
config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"passwordHash\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"password_hash\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"role\",\"kind\":\"enum\",\"type\":\"UserRole\"},{\"name\":\"isActive\",\"kind\":\"scalar\",\"type\":\"Boolean\",\"dbName\":\"is_active\"},{\"name\":\"lastLoginAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"last_login_at\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"userLicenses\",\"kind\":\"object\",\"type\":\"UserLicense\",\"relationName\":\"UserToUserLicense\"}],\"dbName\":\"users\"},\"License\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"code\",\"kind\":\"enum\",\"type\":\"LicenseCode\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"defaultDurationDays\",\"kind\":\"scalar\",\"type\":\"Int\",\"dbName\":\"default_duration_days\"},{\"name\":\"isActive\",\"kind\":\"scalar\",\"type\":\"Boolean\",\"dbName\":\"is_active\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"userLicenses\",\"kind\":\"object\",\"type\":\"UserLicense\",\"relationName\":\"LicenseToUserLicense\"}],\"dbName\":\"licenses\"},\"UserLicense\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"user_id\"},{\"name\":\"licenseId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"license_id\"},{\"name\":\"startsAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"starts_at\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"expires_at\"},{\"name\":\"assignedById\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"assigned_by\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserLicense\"},{\"name\":\"license\",\"kind\":\"object\",\"type\":\"License\",\"relationName\":\"LicenseToUserLicense\"}],\"dbName\":\"user_licenses\"}},\"enums\":{},\"types\":{}}")
defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
config.engineWasm = {
getRuntime: async () => require('./query_engine_bg.js'),
getQueryEngineWasmModule: async () => {
const loader = (await import('#wasm-engine-loader')).default
const engine = (await loader).default
return engine
}
}
config.compilerWasm = undefined
config.injectableEdgeEnv = () => ({
parsed: {
AUTH_DATABASE_URL: typeof globalThis !== 'undefined' && globalThis['AUTH_DATABASE_URL'] || typeof process !== 'undefined' && process.env && process.env.AUTH_DATABASE_URL || undefined
}
})
if (typeof globalThis !== 'undefined' && globalThis['DEBUG'] || typeof process !== 'undefined' && process.env && process.env.DEBUG || undefined) {
Debug.enable(typeof globalThis !== 'undefined' && globalThis['DEBUG'] || typeof process !== 'undefined' && process.env && process.env.DEBUG || undefined)
}
const PrismaClient = getPrismaClient(config)
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)
-1
View File
@@ -1 +0,0 @@
export * from "./index"
-5
View File
@@ -1,5 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('.') }
-1
View File
@@ -1 +0,0 @@
export * from "./index"
-5
View File
@@ -1,5 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('#main-entry-point') }
-1
View File
@@ -1 +0,0 @@
export * from "./default"
File diff suppressed because one or more lines are too long
@@ -1,233 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
objectEnumValues,
makeStrictEnum,
Public,
getRuntime,
skip
} = require('./runtime/index-browser.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.19.3
* Query Engine version: c2990dca591cba766e3b7ef5d9e8a84796e47ab7
*/
Prisma.prismaVersion = {
client: "6.19.3",
engine: "c2990dca591cba766e3b7ef5d9e8a84796e47ab7"
}
Prisma.PrismaClientKnownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)};
Prisma.PrismaClientUnknownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientRustPanicError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientInitializationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientValidationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.empty = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.join = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.raw = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.defineExtension = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull
Prisma.NullTypes = {
DbNull: objectEnumValues.classes.DbNull,
JsonNull: objectEnumValues.classes.JsonNull,
AnyNull: objectEnumValues.classes.AnyNull
}
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
exports.Prisma.LauncherNoticeScalarFieldEnum = {
id: 'id',
appKey: 'appKey',
title: 'title',
body: 'body',
linkUrl: 'linkUrl',
isActive: 'isActive',
startsAt: 'startsAt',
endsAt: 'endsAt',
priority: 'priority',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.ProvisionedDeviceScalarFieldEnum = {
id: 'id',
accountEmail: 'accountEmail',
authUserId: 'authUserId',
hostname: 'hostname',
tailscaleHostname: 'tailscaleHostname',
sshUser: 'sshUser',
workspaceRoot: 'workspaceRoot',
usageType: 'usageType',
os: 'os',
arch: 'arch',
kernel: 'kernel',
distro: 'distro',
device: 'device',
installedAt: 'installedAt',
lastProvisionedAt: 'lastProvisionedAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.LauncherSshKeyScalarFieldEnum = {
id: 'id',
accountEmail: 'accountEmail',
authUserId: 'authUserId',
publicKey: 'publicKey',
publicKeyHash: 'publicKeyHash',
keyType: 'keyType',
label: 'label',
lastProvisionedAt: 'lastProvisionedAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.NullableJsonNullValueInput = {
DbNull: Prisma.DbNull,
JsonNull: Prisma.JsonNull
};
exports.Prisma.QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Prisma.JsonNullValueFilter = {
DbNull: Prisma.DbNull,
JsonNull: Prisma.JsonNull,
AnyNull: Prisma.AnyNull
};
exports.Prisma.ModelName = {
LauncherNotice: 'LauncherNotice',
ProvisionedDevice: 'ProvisionedDevice',
LauncherSshKey: 'LauncherSshKey'
};
/**
* This is a stub Prisma Client that will error at runtime if called.
*/
class PrismaClient {
constructor() {
return new Proxy(this, {
get(target, prop) {
let message
const runtime = getRuntime()
if (runtime.isEdge) {
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
- Use Prisma Accelerate: https://pris.ly/d/accelerate
- Use Driver Adapters: https://pris.ly/d/driver-adapters
`;
} else {
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
}
message += `
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
throw new Error(message)
}
})
}
}
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-183
View File
@@ -1,183 +0,0 @@
{
"name": "prisma-client-2cc4fd701019d0254ce12699ae25121c6ed85eba27006e31abbdac7779ae92f0",
"main": "index.js",
"types": "index.d.ts",
"browser": "default.js",
"exports": {
"./client": {
"require": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./package.json": "./package.json",
".": {
"require": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
},
"./react-native": {
"types": "./react-native.d.ts",
"require": "./react-native.js",
"import": "./react-native.js",
"default": "./react-native.js"
},
"./extension": {
"types": "./extension.d.ts",
"require": "./extension.js",
"import": "./extension.js",
"default": "./extension.js"
},
"./index-browser": {
"types": "./index.d.ts",
"require": "./index-browser.js",
"import": "./index-browser.js",
"default": "./index-browser.js"
},
"./index": {
"types": "./index.d.ts",
"require": "./index.js",
"import": "./index.js",
"default": "./index.js"
},
"./wasm": {
"types": "./wasm.d.ts",
"require": "./wasm.js",
"import": "./wasm.mjs",
"default": "./wasm.mjs"
},
"./runtime/client": {
"types": "./runtime/client.d.ts",
"node": {
"require": "./runtime/client.js",
"default": "./runtime/client.js"
},
"require": "./runtime/client.js",
"import": "./runtime/client.mjs",
"default": "./runtime/client.mjs"
},
"./runtime/library": {
"types": "./runtime/library.d.ts",
"require": "./runtime/library.js",
"import": "./runtime/library.mjs",
"default": "./runtime/library.mjs"
},
"./runtime/binary": {
"types": "./runtime/binary.d.ts",
"require": "./runtime/binary.js",
"import": "./runtime/binary.mjs",
"default": "./runtime/binary.mjs"
},
"./runtime/wasm-engine-edge": {
"types": "./runtime/wasm-engine-edge.d.ts",
"require": "./runtime/wasm-engine-edge.js",
"import": "./runtime/wasm-engine-edge.mjs",
"default": "./runtime/wasm-engine-edge.mjs"
},
"./runtime/wasm-compiler-edge": {
"types": "./runtime/wasm-compiler-edge.d.ts",
"require": "./runtime/wasm-compiler-edge.js",
"import": "./runtime/wasm-compiler-edge.mjs",
"default": "./runtime/wasm-compiler-edge.mjs"
},
"./runtime/edge": {
"types": "./runtime/edge.d.ts",
"require": "./runtime/edge.js",
"import": "./runtime/edge-esm.js",
"default": "./runtime/edge-esm.js"
},
"./runtime/react-native": {
"types": "./runtime/react-native.d.ts",
"require": "./runtime/react-native.js",
"import": "./runtime/react-native.js",
"default": "./runtime/react-native.js"
},
"./runtime/index-browser": {
"types": "./runtime/index-browser.d.ts",
"require": "./runtime/index-browser.js",
"import": "./runtime/index-browser.mjs",
"default": "./runtime/index-browser.mjs"
},
"./generator-build": {
"require": "./generator-build/index.js",
"import": "./generator-build/index.js",
"default": "./generator-build/index.js"
},
"./sql": {
"require": {
"types": "./sql.d.ts",
"node": "./sql.js",
"default": "./sql.js"
},
"import": {
"types": "./sql.d.ts",
"node": "./sql.mjs",
"default": "./sql.mjs"
},
"default": "./sql.js"
},
"./*": "./*"
},
"version": "6.19.3",
"sideEffects": false,
"imports": {
"#wasm-engine-loader": {
"edge-light": "./wasm-edge-light-loader.mjs",
"workerd": "./wasm-worker-loader.mjs",
"worker": "./wasm-worker-loader.mjs",
"default": "./wasm-worker-loader.mjs"
},
"#main-entry-point": {
"require": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
}
}
}
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-370
View File
@@ -1,370 +0,0 @@
declare class AnyNull extends NullTypesEnumValue {
#private;
}
declare type Args<T, F extends Operation> = T extends {
[K: symbol]: {
types: {
operations: {
[K in F]: {
args: any;
};
};
};
};
} ? T[symbol]['types']['operations'][F]['args'] : any;
declare class DbNull extends NullTypesEnumValue {
#private;
}
export declare function Decimal(n: Decimal.Value): Decimal;
export declare namespace Decimal {
export type Constructor = typeof Decimal;
export type Instance = Decimal;
export type Rounding = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export type Modulo = Rounding | 9;
export type Value = string | number | Decimal;
// http://mikemcl.github.io/decimal.js/#constructor-properties
export interface Config {
precision?: number;
rounding?: Rounding;
toExpNeg?: number;
toExpPos?: number;
minE?: number;
maxE?: number;
crypto?: boolean;
modulo?: Modulo;
defaults?: boolean;
}
}
export declare class Decimal {
readonly d: number[];
readonly e: number;
readonly s: number;
constructor(n: Decimal.Value);
absoluteValue(): Decimal;
abs(): Decimal;
ceil(): Decimal;
clampedTo(min: Decimal.Value, max: Decimal.Value): Decimal;
clamp(min: Decimal.Value, max: Decimal.Value): Decimal;
comparedTo(n: Decimal.Value): number;
cmp(n: Decimal.Value): number;
cosine(): Decimal;
cos(): Decimal;
cubeRoot(): Decimal;
cbrt(): Decimal;
decimalPlaces(): number;
dp(): number;
dividedBy(n: Decimal.Value): Decimal;
div(n: Decimal.Value): Decimal;
dividedToIntegerBy(n: Decimal.Value): Decimal;
divToInt(n: Decimal.Value): Decimal;
equals(n: Decimal.Value): boolean;
eq(n: Decimal.Value): boolean;
floor(): Decimal;
greaterThan(n: Decimal.Value): boolean;
gt(n: Decimal.Value): boolean;
greaterThanOrEqualTo(n: Decimal.Value): boolean;
gte(n: Decimal.Value): boolean;
hyperbolicCosine(): Decimal;
cosh(): Decimal;
hyperbolicSine(): Decimal;
sinh(): Decimal;
hyperbolicTangent(): Decimal;
tanh(): Decimal;
inverseCosine(): Decimal;
acos(): Decimal;
inverseHyperbolicCosine(): Decimal;
acosh(): Decimal;
inverseHyperbolicSine(): Decimal;
asinh(): Decimal;
inverseHyperbolicTangent(): Decimal;
atanh(): Decimal;
inverseSine(): Decimal;
asin(): Decimal;
inverseTangent(): Decimal;
atan(): Decimal;
isFinite(): boolean;
isInteger(): boolean;
isInt(): boolean;
isNaN(): boolean;
isNegative(): boolean;
isNeg(): boolean;
isPositive(): boolean;
isPos(): boolean;
isZero(): boolean;
lessThan(n: Decimal.Value): boolean;
lt(n: Decimal.Value): boolean;
lessThanOrEqualTo(n: Decimal.Value): boolean;
lte(n: Decimal.Value): boolean;
logarithm(n?: Decimal.Value): Decimal;
log(n?: Decimal.Value): Decimal;
minus(n: Decimal.Value): Decimal;
sub(n: Decimal.Value): Decimal;
modulo(n: Decimal.Value): Decimal;
mod(n: Decimal.Value): Decimal;
naturalExponential(): Decimal;
exp(): Decimal;
naturalLogarithm(): Decimal;
ln(): Decimal;
negated(): Decimal;
neg(): Decimal;
plus(n: Decimal.Value): Decimal;
add(n: Decimal.Value): Decimal;
precision(includeZeros?: boolean): number;
sd(includeZeros?: boolean): number;
round(): Decimal;
sine() : Decimal;
sin() : Decimal;
squareRoot(): Decimal;
sqrt(): Decimal;
tangent() : Decimal;
tan() : Decimal;
times(n: Decimal.Value): Decimal;
mul(n: Decimal.Value) : Decimal;
toBinary(significantDigits?: number): string;
toBinary(significantDigits: number, rounding: Decimal.Rounding): string;
toDecimalPlaces(decimalPlaces?: number): Decimal;
toDecimalPlaces(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
toDP(decimalPlaces?: number): Decimal;
toDP(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
toExponential(decimalPlaces?: number): string;
toExponential(decimalPlaces: number, rounding: Decimal.Rounding): string;
toFixed(decimalPlaces?: number): string;
toFixed(decimalPlaces: number, rounding: Decimal.Rounding): string;
toFraction(max_denominator?: Decimal.Value): Decimal[];
toHexadecimal(significantDigits?: number): string;
toHexadecimal(significantDigits: number, rounding: Decimal.Rounding): string;
toHex(significantDigits?: number): string;
toHex(significantDigits: number, rounding?: Decimal.Rounding): string;
toJSON(): string;
toNearest(n: Decimal.Value, rounding?: Decimal.Rounding): Decimal;
toNumber(): number;
toOctal(significantDigits?: number): string;
toOctal(significantDigits: number, rounding: Decimal.Rounding): string;
toPower(n: Decimal.Value): Decimal;
pow(n: Decimal.Value): Decimal;
toPrecision(significantDigits?: number): string;
toPrecision(significantDigits: number, rounding: Decimal.Rounding): string;
toSignificantDigits(significantDigits?: number): Decimal;
toSignificantDigits(significantDigits: number, rounding: Decimal.Rounding): Decimal;
toSD(significantDigits?: number): Decimal;
toSD(significantDigits: number, rounding: Decimal.Rounding): Decimal;
toString(): string;
truncated(): Decimal;
trunc(): Decimal;
valueOf(): string;
static abs(n: Decimal.Value): Decimal;
static acos(n: Decimal.Value): Decimal;
static acosh(n: Decimal.Value): Decimal;
static add(x: Decimal.Value, y: Decimal.Value): Decimal;
static asin(n: Decimal.Value): Decimal;
static asinh(n: Decimal.Value): Decimal;
static atan(n: Decimal.Value): Decimal;
static atanh(n: Decimal.Value): Decimal;
static atan2(y: Decimal.Value, x: Decimal.Value): Decimal;
static cbrt(n: Decimal.Value): Decimal;
static ceil(n: Decimal.Value): Decimal;
static clamp(n: Decimal.Value, min: Decimal.Value, max: Decimal.Value): Decimal;
static clone(object?: Decimal.Config): Decimal.Constructor;
static config(object: Decimal.Config): Decimal.Constructor;
static cos(n: Decimal.Value): Decimal;
static cosh(n: Decimal.Value): Decimal;
static div(x: Decimal.Value, y: Decimal.Value): Decimal;
static exp(n: Decimal.Value): Decimal;
static floor(n: Decimal.Value): Decimal;
static hypot(...n: Decimal.Value[]): Decimal;
static isDecimal(object: any): object is Decimal;
static ln(n: Decimal.Value): Decimal;
static log(n: Decimal.Value, base?: Decimal.Value): Decimal;
static log2(n: Decimal.Value): Decimal;
static log10(n: Decimal.Value): Decimal;
static max(...n: Decimal.Value[]): Decimal;
static min(...n: Decimal.Value[]): Decimal;
static mod(x: Decimal.Value, y: Decimal.Value): Decimal;
static mul(x: Decimal.Value, y: Decimal.Value): Decimal;
static noConflict(): Decimal.Constructor; // Browser only
static pow(base: Decimal.Value, exponent: Decimal.Value): Decimal;
static random(significantDigits?: number): Decimal;
static round(n: Decimal.Value): Decimal;
static set(object: Decimal.Config): Decimal.Constructor;
static sign(n: Decimal.Value): number;
static sin(n: Decimal.Value): Decimal;
static sinh(n: Decimal.Value): Decimal;
static sqrt(n: Decimal.Value): Decimal;
static sub(x: Decimal.Value, y: Decimal.Value): Decimal;
static sum(...n: Decimal.Value[]): Decimal;
static tan(n: Decimal.Value): Decimal;
static tanh(n: Decimal.Value): Decimal;
static trunc(n: Decimal.Value): Decimal;
static readonly default?: Decimal.Constructor;
static readonly Decimal?: Decimal.Constructor;
static readonly precision: number;
static readonly rounding: Decimal.Rounding;
static readonly toExpNeg: number;
static readonly toExpPos: number;
static readonly minE: number;
static readonly maxE: number;
static readonly crypto: boolean;
static readonly modulo: Decimal.Modulo;
static readonly ROUND_UP: 0;
static readonly ROUND_DOWN: 1;
static readonly ROUND_CEIL: 2;
static readonly ROUND_FLOOR: 3;
static readonly ROUND_HALF_UP: 4;
static readonly ROUND_HALF_DOWN: 5;
static readonly ROUND_HALF_EVEN: 6;
static readonly ROUND_HALF_CEIL: 7;
static readonly ROUND_HALF_FLOOR: 8;
static readonly EUCLID: 9;
}
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
[K in keyof A]: Exact<A[K], W[K]>;
} : W) : never) | (A extends Narrowable ? A : never);
export declare function getRuntime(): GetRuntimeOutput;
declare type GetRuntimeOutput = {
id: RuntimeName;
prettyName: string;
isEdge: boolean;
};
declare class JsonNull extends NullTypesEnumValue {
#private;
}
/**
* Generates more strict variant of an enum which, unlike regular enum,
* throws on non-existing property access. This can be useful in following situations:
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
* - enum values are generated dynamically from DMMF.
*
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
* will result in `undefined` value being used, which will be accepted. Using strict enum
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
*
* Note: if you need to check for existence of a value in the enum you can still use either
* `in` operator or `hasOwnProperty` function.
*
* @param definition
* @returns
*/
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
declare type Narrowable = string | number | bigint | boolean | [];
declare class NullTypesEnumValue extends ObjectEnumValue {
_getNamespace(): string;
}
/**
* Base class for unique values of object-valued enums.
*/
declare abstract class ObjectEnumValue {
constructor(arg?: symbol);
abstract _getNamespace(): string;
_getName(): string;
toString(): string;
}
export declare const objectEnumValues: {
classes: {
DbNull: typeof DbNull;
JsonNull: typeof JsonNull;
AnyNull: typeof AnyNull;
};
instances: {
DbNull: DbNull;
JsonNull: JsonNull;
AnyNull: AnyNull;
};
};
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'updateManyAndReturn' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';
declare namespace Public {
export {
validator
}
}
export { Public }
declare type RuntimeName = 'workerd' | 'deno' | 'netlify' | 'node' | 'bun' | 'edge-light' | '';
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation, P extends keyof Args<C[M], O>>(client: C, model: M, operation: O, prop: P): <S>(select: Exact<S, Args<C[M], O>[P]>) => S;
export { }
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,68 +0,0 @@
generator launcherClient {
provider = "prisma-client-js"
output = "../src/generated/launcher-client"
}
datasource db {
provider = "postgresql"
url = env("LAUNCHER_DATABASE_URL")
}
model LauncherNotice {
id String @id @default(uuid()) @db.Uuid
appKey String @default("login") @map("app_key")
title String
body String
linkUrl String? @map("link_url")
isActive Boolean @default(true) @map("is_active")
startsAt DateTime? @map("starts_at") @db.Timestamptz(3)
endsAt DateTime? @map("ends_at") @db.Timestamptz(3)
priority Int @default(0)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
@@index([appKey, isActive, priority])
@@map("launcher_notices")
}
model ProvisionedDevice {
id String @id @default(uuid()) @db.Uuid
accountEmail String @map("account_email")
authUserId String? @map("auth_user_id") @db.Uuid
hostname String
tailscaleHostname String @map("tailscale_hostname")
sshUser String? @map("ssh_user")
workspaceRoot String? @map("workspace_root")
usageType String @default("personal") @map("usage_type")
os String?
arch String?
kernel String?
distro String?
device Json?
installedAt DateTime @default(now()) @map("installed_at") @db.Timestamptz(3)
lastProvisionedAt DateTime @default(now()) @map("last_provisioned_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
@@unique([accountEmail, tailscaleHostname])
@@index([accountEmail])
@@index([tailscaleHostname])
@@map("provisioned_devices")
}
model LauncherSshKey {
id String @id @default(uuid()) @db.Uuid
accountEmail String @map("account_email")
authUserId String? @map("auth_user_id") @db.Uuid
publicKey String @map("public_key")
publicKeyHash String @map("public_key_hash")
keyType String? @map("key_type")
label String?
lastProvisionedAt DateTime @default(now()) @map("last_provisioned_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
@@unique([accountEmail, publicKeyHash])
@@index([accountEmail])
@@map("launcher_ssh_keys")
}
@@ -1,5 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_engine_bg.wasm?module')
@@ -1,5 +0,0 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_engine_bg.wasm')

Some files were not shown because too many files have changed in this diff Show More