Files
KodekaTeam 87fdbb493b feat(sessions): implement user session management with session tracking and revocation
- Introduced new database schema for user sessions and session events.
- Implemented session management page with filtering, sorting, and revocation capabilities.
- Added backend services for creating, revoking, and expiring user sessions.
- Implemented session cookie handling and security measures.
- Created tests for session management functionalities and security features.
2026-09-11 13:34:01 +07:00

948 lines
27 KiB
Plaintext

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ========================
// Auth
// ========================
model User {
id String @id @default(cuid())
email String @unique
name String
passwordHash String
role UserRole @default(VIEWER)
isActive Boolean @default(true)
allServersAccess Boolean @default(true)
activeOrganizationId String?
lastLogin DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
activeOrganization Organization? @relation("UserActiveOrganization", fields: [activeOrganizationId], references: [id], onDelete: SetNull)
organizationMemberships OrganizationMembership[]
createdOrganizations Organization[] @relation("OrganizationCreator")
apiKeys ApiKey[]
auditLogs AuditLog[]
deployments Deployment[]
settings UserSettings?
notificationProviders UserNotificationProvider[]
integrations UserIntegration[]
gitProviders UserGitProvider[]
storageDestinations UserStorageDestination[]
serverAssignments UserServerAccess[]
sentInvitations UserInvitation[] @relation("InvitationCreator")
sessions UserSession[]
@@index([activeOrganizationId])
@@map("users")
}
model Organization {
id String @id @default(cuid())
name String
slug String @unique
logoUrl String?
createdById String?
createdBy User? @relation("OrganizationCreator", fields: [createdById], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
members OrganizationMembership[]
activeUsers User[] @relation("UserActiveOrganization")
servers Server[]
projects Project[]
domains Domain[]
apiKeys ApiKey[]
invitations UserInvitation[]
auditLogs AuditLog[]
deployments Deployment[]
@@index([createdById])
@@map("organizations")
}
model OrganizationMembership {
id String @id @default(cuid())
organizationId String
userId String
isDefault Boolean @default(false)
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([organizationId, userId])
@@index([userId])
@@map("organization_memberships")
}
// ========================
// Projects
// ========================
model Project {
id String @id @default(cuid())
organizationId String
name String
slug String
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
environments Environment[]
deployments Deployment[]
@@unique([organizationId, slug])
@@index([organizationId])
@@map("projects")
}
model Environment {
id String @id @default(cuid())
projectId String
serverId String
name String
slug String
kind EnvironmentKind
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
server Server @relation(fields: [serverId], references: [id], onDelete: Restrict)
containers Container[]
deployments Deployment[]
@@unique([projectId, slug])
@@index([projectId])
@@index([projectId, kind])
@@index([serverId])
@@map("environments")
}
enum EnvironmentKind {
PRODUCTION
STAGING
DEVELOPMENT
PREVIEW
CUSTOM
}
model UserSettings {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
panelName String @default("DOKTAINER")
panelUrl String @default("http://localhost:3000")
timezone String @default("Asia/Jakarta")
theme String @default("dark")
sessionTimeoutMinutes Int @default(30)
twoFactorEnabled Boolean @default(false)
twoFactorSecretEnc String?
twoFactorPendingSecretEnc String?
ipWhitelistEnabled Boolean @default(false)
ipWhitelist String[] @default([])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("user_settings")
}
model UserNotificationProvider {
id String @id @default(cuid())
userId String
organizationId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
type NotificationProviderType
name String
enabled Boolean @default(true)
actions String[] @default([])
channel String?
webhookUrlEnc String?
smtpHost String?
smtpPort Int @default(587)
smtpSecure Boolean @default(false)
smtpUsername String?
smtpPasswordEnc String?
smtpFromEmail String?
smtpFromName String?
telegramChatId String?
telegramBotTokenEnc String?
serverUrl String?
topic String?
userKey String?
apiKeyEnc String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId, organizationId, type])
@@index([organizationId])
@@map("user_notification_providers")
}
model UserIntegration {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
provider IntegrationProviderType
connected Boolean @default(false)
zoneId String?
apiTokenEnc String?
accessKeyId String?
secretAccessKeyEnc String?
region String?
bucket String?
tokenEnc String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, provider])
@@index([userId])
@@map("user_integrations")
}
model UserStorageDestination {
id String @id @default(cuid())
userId String
organizationId String
serverId String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
server Server? @relation(fields: [serverId], references: [id], onDelete: SetNull)
name String
provider S3StorageProvider
enabled Boolean @default(true)
accessKeyId String
secretAccessKeyEnc String?
region String?
bucket String
endpoint String?
additionalFlags String[] @default([])
retentionDays Int?
maxBackupCount Int?
objectPrefix String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId, organizationId])
@@index([organizationId, serverId])
@@map("user_storage_destinations")
}
model UserGitProvider {
id String @id @default(cuid())
userId String
organizationId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
containerDeploymentSources ContainerDeploymentSource[]
provider GitProviderType
name String
enabled Boolean @default(true)
appName String
appId String?
clientId String?
clientSecretEnc String?
webhookSecretEnc String?
appUrl String?
installationUrl String?
providerUrl String?
internalUrl String?
accountUsername String?
accountEmail String?
namespace String?
organizationScoped Boolean @default(false)
organizationName String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId, organizationId, provider])
@@index([organizationId])
@@map("user_git_providers")
}
model UserServerAccess {
id String @id @default(cuid())
userId String
serverId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([userId, serverId])
@@index([serverId])
@@map("user_server_access")
}
model UserInvitation {
id String @id @default(cuid())
organizationId String
email String
name String
role UserRole
tokenHash String @unique
allServersAccess Boolean @default(true)
serverIds String[]
invitedById String?
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
invitedBy User? @relation("InvitationCreator", fields: [invitedById], references: [id], onDelete: SetNull)
expiresAt DateTime
acceptedAt DateTime?
revokedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
@@index([expiresAt])
@@index([organizationId])
@@map("user_invitations")
}
enum UserRole {
SUPER_ADMIN
OPERATOR
DEVELOPER
VIEWER
}
enum NotificationProviderType {
SLACK
TELEGRAM
DISCORD
LARK
TEAMS
EMAIL
RESEND
GOTIFY
NTFY
MATTERMOST
PUSHOVER
CUSTOM
}
enum IntegrationProviderType {
CLOUDFLARE
AWS_S3
GITHUB
GITLAB
}
enum GitProviderType {
GITHUB
GITLAB
BITBUCKET
GITEA
}
enum S3StorageProvider {
AWS_S3
ALIBABA_OSS
ARVAN_AOS
CEPH
CHINA_MOBILE_EOS
CLOUDFLARE_R2
DIGITALOCEAN_SPACES
DREAMOBJECTS
GOOGLE_CLOUD_STORAGE
HUAWEI_OBS
IBM_COS
CUSTOM
}
model ApiKey {
id String @id @default(cuid())
organizationId String
name String
keyHash String @unique
keyPrefix String
permissions String[]
userId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
lastUsed DateTime?
expiresAt DateTime?
isActive Boolean @default(true)
requestCount Int @default(0)
createdAt DateTime @default(now())
@@index([organizationId])
@@map("api_keys")
}
// ========================
// Servers
// ========================
model Server {
id String @id @default(cuid())
organizationId String
name String
ip String
sshPort Int @default(22)
username String @default("root")
authType SshAuthType @default(PASSWORD)
sshKeyEnc String? // encrypted private key
passwordEnc String? // encrypted password
status ServerStatus @default(UNKNOWN)
os String?
location String?
tags String[]
lastHealthAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
containers Container[]
environments Environment[]
networks Network[]
storageDestinations UserStorageDestination[]
domains Domain[]
auditLogs AuditLog[]
deployments Deployment[]
metrics ServerMetric[]
appInstalls AppInstall[]
backups Backup[]
firewallRulePresets FirewallRulePreset[]
userAssignments UserServerAccess[]
@@index([organizationId])
@@map("servers")
}
enum SshAuthType {
PASSWORD
SSH_KEY
}
enum ServerStatus {
ONLINE
OFFLINE
WARNING
UNKNOWN
}
model ServerMetric {
id String @id @default(cuid())
serverId String
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
cpuPct Float
ramPct Float
diskPct Float
ramUsed BigInt
ramTotal BigInt
diskUsed BigInt
diskTotal BigInt
networkRxBps BigInt?
networkTxBps BigInt?
uptimeSec BigInt
recordedAt DateTime @default(now())
@@index([serverId, recordedAt])
@@map("server_metrics")
}
// ========================
// Containers
// ========================
model Container {
id String @id @default(cuid())
dockerId String?
name String
image String
status ContainerStatus @default(STOPPED)
sourceType ContainerSourceType @default(MANUAL)
deployMode ContainerDeployMode?
serverId String
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
environmentId String?
environment Environment? @relation(fields: [environmentId], references: [id], onDelete: SetNull)
domains Domain[]
deploymentSource ContainerDeploymentSource?
deployments Deployment[]
deploymentLock DeploymentLock?
ports Json @default("[]") // [{host: 80, container: 80, protocol: "tcp"}]
envVars Json @default("[]") // [{key: "NODE_ENV", value: "production"}]
volumes Json @default("[]")
labels Json @default("{}")
restartPolicy String @default("unless-stopped")
cpuUsage String?
ramUsage String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([environmentId])
@@index([serverId])
@@map("containers")
}
model ContainerDeploymentSource {
id String @id @default(cuid())
containerId String @unique
container Container @relation(fields: [containerId], references: [id], onDelete: Cascade)
projectName String?
gitProviderId String?
gitProvider UserGitProvider? @relation(fields: [gitProviderId], references: [id], onDelete: SetNull)
repoUrl String?
repoBranch String?
repoVisibility RepositoryVisibility?
buildType String?
buildPath String?
startCommand String?
portOverride String?
publishDirectory String?
imageTag String?
accessTokenEnc String?
composeEnvOverrides Json?
projectPath String?
composeFilePath String?
dockerfilePath String?
dockerContextPath String?
deploymentPath String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([gitProviderId])
@@map("container_deployment_sources")
}
model Deployment {
id String @id @default(cuid())
containerId String
organizationId String
serverId String
projectId String?
environmentId String?
userId String?
idempotencyKey String?
status DeploymentStatus
trigger DeploymentTrigger
version String?
commitSha String?
branch String?
image String?
imageDigest String?
sourceSnapshot Json?
configSnapshot Json
configRevision String?
rollbackSnapshotEnc String?
strategy String?
candidateRef String?
previousDeploymentId String?
failureReason String?
rollbackReason String?
logReference String?
error String?
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
container Container @relation(fields: [containerId], references: [id], onDelete: Cascade)
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
environment Environment? @relation(fields: [environmentId], references: [id], onDelete: SetNull)
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
previousDeployment Deployment? @relation("DeploymentPredecessor", fields: [previousDeploymentId], references: [id], onDelete: SetNull)
nextDeployments Deployment[] @relation("DeploymentPredecessor")
events DeploymentEvent[]
@@unique([containerId, idempotencyKey])
@@index([containerId, createdAt])
@@index([containerId, status, createdAt])
@@index([organizationId, createdAt])
@@index([serverId, createdAt])
@@index([projectId, createdAt])
@@index([environmentId, createdAt])
@@index([previousDeploymentId])
@@map("deployments")
}
model UserSession {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tokenHash String @unique
createdIp String?
lastIp String?
userAgent String?
createdAt DateTime @default(now())
lastSeenAt DateTime @default(now())
idleExpiresAt DateTime
absoluteExpiresAt DateTime
revokedAt DateTime?
revokeReason String?
events UserSessionEvent[]
@@index([userId, revokedAt])
@@index([idleExpiresAt])
@@index([absoluteExpiresAt])
@@map("user_sessions")
}
model UserSessionEvent {
id String @id @default(cuid())
sessionId String
session UserSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
type SessionEventType
ipAddress String?
userAgent String?
details Json?
createdAt DateTime @default(now())
@@index([sessionId, createdAt])
@@map("user_session_events")
}
enum SessionEventType {
LOGIN
LOGOUT
REVOKED
EXPIRED
}
model DeploymentEvent {
id String @id @default(cuid())
deploymentId String
status DeploymentStatus
level DeploymentEventLevel @default(INFO)
message String
metadata Json?
createdAt DateTime @default(now())
deployment Deployment @relation(fields: [deploymentId], references: [id], onDelete: Cascade)
@@index([deploymentId, createdAt])
@@map("deployment_events")
}
model DeploymentLock {
containerId String @id
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
container Container @relation(fields: [containerId], references: [id], onDelete: Cascade)
@@index([expiresAt])
@@map("deployment_locks")
}
enum DeploymentStatus {
QUEUED
BUILDING
RUNNING
VALIDATING
SWITCHING
ACTIVE
SUCCESS
FAILED
FAILED_ROLLED_BACK
CANCELLED
ROLLBACK_RUNNING
ROLLED_BACK
SUPERSEDED
}
enum DeploymentEventLevel {
INFO
WARNING
ERROR
}
enum DeploymentTrigger {
MANUAL
GIT_WEBHOOK
REBUILD
REDEPLOY
CONFIG_APPLY
ROLLBACK
APP_INSTALLER
}
model Network {
id String @id @default(cuid())
name String
driver String @default("bridge")
scope String @default("local")
subnet String?
gateway String?
containers Int @default(0)
serverId String
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([serverId, name])
@@index([serverId])
@@map("networks")
}
model FirewallRulePreset {
id String @id @default(cuid())
serverId String
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
rule String
action String
direction String @default("IN")
source String @default("Anywhere")
description String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([serverId, rule, action, direction, source])
@@index([serverId])
@@map("firewall_rule_presets")
}
enum ContainerStatus {
RUNNING
STOPPED
STARTING
STOPPING
ERROR
PAUSED
}
enum ContainerSourceType {
APP_INSTALLER
MANUAL
GIT_CLONE
GIT_PROVIDER
}
enum ContainerDeployMode {
IMAGE
DOCKERFILE
COMPOSE
}
enum RepositoryVisibility {
PUBLIC
PRIVATE
}
// ========================
// Domains & SSL
// ========================
model Domain {
id String @id @default(cuid())
name String @unique
organizationId String
domainNameType DomainNameType @default(EXACT)
configMode DomainConfigMode @default(ISOLATED)
reviewStatus DomainReviewStatus @default(CONFIRMED)
isPrimary Boolean @default(false)
type DnsType @default(A)
value String // IP or CNAME target
serverId String?
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
server Server? @relation(fields: [serverId], references: [id])
targetContainerId String?
targetContainer Container? @relation(fields: [targetContainerId], references: [id], onDelete: SetNull)
targetPort Int?
proxy ProxyType @default(NONE)
discoverySource DomainDiscoverySource @default(MANUAL)
sslEnabled Boolean @default(false)
autoRenew Boolean @default(true)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sslCert SslCert?
@@index([organizationId])
@@index([targetContainerId])
@@map("domains")
}
enum DnsType {
A
AAAA
CNAME
MX
TXT
}
enum ProxyType {
TRAEFIK
NGINX
CADDY
NONE
}
enum DomainDiscoverySource {
MANUAL
NGINX
TRAEFIK
CADDY
CADDY_ADMIN
CERTBOT
}
enum DomainNameType {
EXACT
WILDCARD
}
enum DomainConfigMode {
SHARED
ISOLATED
}
enum DomainReviewStatus {
CONFIRMED
NEEDS_REVIEW
}
model SslCert {
id String @id @default(cuid())
domainId String @unique
domain Domain @relation(fields: [domainId], references: [id], onDelete: Cascade)
issuer String @default("Let's Encrypt")
certPem String? // certificate PEM
keyPem String? // private key PEM (encrypted)
issuedAt DateTime?
expiresAt DateTime?
status SslStatus @default(PENDING)
autoRenew Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("ssl_certs")
}
enum SslStatus {
VALID
EXPIRING
EXPIRED
PENDING
FAILED
}
// ========================
// Audit Log
// ========================
model AuditLog {
id String @id @default(cuid())
userId String?
organizationId String?
user User? @relation(fields: [userId], references: [id])
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
serverId String?
server Server? @relation(fields: [serverId], references: [id])
action String
category LogCategory
level LogLevel @default(INFO)
message String
meta Json?
createdAt DateTime @default(now())
@@index([createdAt])
@@index([organizationId])
@@index([serverId])
@@map("audit_logs")
}
enum LogCategory {
AUTH
SERVER
CONTAINER
DOMAIN
SSL
SECURITY
SYSTEM
TERMINAL
}
enum LogLevel {
DEBUG
INFO
WARNING
ERROR
SUCCESS
}
// ========================
// App Installer
// ========================
model AppInstall {
id String @id @default(cuid())
appId String // e.g. "wordpress", "nginx"
appName String
serverId String
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
containerName String? // docker container name
port String? // host port used
status AppInstallStatus @default(PENDING)
error String?
installedAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("app_installs")
}
enum AppInstallStatus {
PENDING
INSTALLING
RUNNING
STARTING
STOPPING
STOPPED
PAUSED
UNKNOWN
FAILED
REMOVED
}
// ========================
// Backups
// ========================
model Backup {
id String @id @default(cuid())
name String
type BackupType
databaseEngine DatabaseEngine?
serverId String
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
target String @default("Local")
sizeMb Float?
filePath String?
storageBucket String?
storageKey String?
storageEtag String?
storageDestinationId String?
status BackupStatus @default(PENDING)
error String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("backups")
}
enum BackupType {
DATABASE
VOLUME
FULL
}
enum DatabaseEngine {
POSTGRESQL
MYSQL
MARIADB
MONGODB
REDIS
}
enum BackupStatus {
PENDING
RUNNING
COMPLETED
FAILED
}