Complete account schema foundation

This commit is contained in:
diyaa
2026-06-24 07:36:57 +02:00
parent 538d259b63
commit babea87140
16 changed files with 789 additions and 57 deletions
@@ -0,0 +1,188 @@
CREATE TYPE "UserAccountKind" AS ENUM ('LEGACY_DEFAULT', 'GUEST', 'REGISTERED');
CREATE TYPE "UserAccountStatus" AS ENUM ('ACTIVE', 'LOCKED', 'DELETED');
ALTER TABLE "users"
ADD COLUMN "account_kind" "UserAccountKind",
ADD COLUMN "account_status" "UserAccountStatus",
ADD COLUMN "library_namespace" UUID;
UPDATE "users"
SET
"account_kind" = 'LEGACY_DEFAULT'::"UserAccountKind",
"account_status" = 'ACTIVE'::"UserAccountStatus",
"library_namespace" = COALESCE("library_namespace", gen_random_uuid())
WHERE "account_kind" IS NULL
OR "account_status" IS NULL
OR "library_namespace" IS NULL;
UPDATE "users"
SET
"account_kind" = 'LEGACY_DEFAULT'::"UserAccountKind",
"account_status" = 'ACTIVE'::"UserAccountStatus"
WHERE "slug" = 'default-owner';
ALTER TABLE "users"
ALTER COLUMN "account_kind" SET NOT NULL,
ALTER COLUMN "account_kind" SET DEFAULT 'LEGACY_DEFAULT'::"UserAccountKind",
ALTER COLUMN "account_status" SET NOT NULL,
ALTER COLUMN "account_status" SET DEFAULT 'ACTIVE'::"UserAccountStatus",
ALTER COLUMN "library_namespace" SET NOT NULL;
CREATE UNIQUE INDEX "users_library_namespace_key"
ON "users"("library_namespace");
CREATE TABLE "user_password_credentials" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"password_hash" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "user_password_credentials_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "user_password_credentials_user_id_key"
ON "user_password_credentials"("user_id");
CREATE TABLE "user_oauth_identities" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"provider" TEXT NOT NULL,
"provider_subject" TEXT NOT NULL,
"email" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "user_oauth_identities_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "user_oauth_identities_provider_provider_subject_key"
ON "user_oauth_identities"("provider", "provider_subject");
CREATE INDEX "user_oauth_identities_user_id_idx"
ON "user_oauth_identities"("user_id");
CREATE TABLE "email_verification_tokens" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"email" TEXT NOT NULL,
"token_hash" TEXT NOT NULL,
"expires_at" TIMESTAMP(3) NOT NULL,
"consumed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "email_verification_tokens_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "email_verification_tokens_token_hash_key"
ON "email_verification_tokens"("token_hash");
CREATE INDEX "email_verification_tokens_user_id_idx"
ON "email_verification_tokens"("user_id");
CREATE TABLE "password_reset_tokens" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"token_hash" TEXT NOT NULL,
"expires_at" TIMESTAMP(3) NOT NULL,
"consumed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "password_reset_tokens_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "password_reset_tokens_token_hash_key"
ON "password_reset_tokens"("token_hash");
CREATE INDEX "password_reset_tokens_user_id_idx"
ON "password_reset_tokens"("user_id");
CREATE TABLE "device_link_history" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"device_id" UUID,
"linked_by_device_id" UUID,
"link_method" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "device_link_history_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "device_link_history_user_id_idx"
ON "device_link_history"("user_id");
CREATE INDEX "device_link_history_device_id_idx"
ON "device_link_history"("device_id");
CREATE TABLE "ownership_transfers" (
"id" UUID NOT NULL,
"from_user_id" UUID NOT NULL,
"to_user_id" UUID,
"requested_by_device_id" UUID,
"status" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"completed_at" TIMESTAMP(3),
CONSTRAINT "ownership_transfers_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "ownership_transfers_from_user_id_idx"
ON "ownership_transfers"("from_user_id");
CREATE INDEX "ownership_transfers_to_user_id_idx"
ON "ownership_transfers"("to_user_id");
ALTER TABLE "user_password_credentials"
ADD CONSTRAINT "user_password_credentials_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id")
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE "user_oauth_identities"
ADD CONSTRAINT "user_oauth_identities_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id")
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE "email_verification_tokens"
ADD CONSTRAINT "email_verification_tokens_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id")
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE "password_reset_tokens"
ADD CONSTRAINT "password_reset_tokens_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id")
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE "device_link_history"
ADD CONSTRAINT "device_link_history_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id")
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE "device_link_history"
ADD CONSTRAINT "device_link_history_device_id_fkey"
FOREIGN KEY ("device_id") REFERENCES "devices"("id")
ON DELETE SET NULL
ON UPDATE CASCADE;
ALTER TABLE "device_link_history"
ADD CONSTRAINT "device_link_history_linked_by_device_id_fkey"
FOREIGN KEY ("linked_by_device_id") REFERENCES "devices"("id")
ON DELETE SET NULL
ON UPDATE CASCADE;
ALTER TABLE "ownership_transfers"
ADD CONSTRAINT "ownership_transfers_from_user_id_fkey"
FOREIGN KEY ("from_user_id") REFERENCES "users"("id")
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE "ownership_transfers"
ADD CONSTRAINT "ownership_transfers_to_user_id_fkey"
FOREIGN KEY ("to_user_id") REFERENCES "users"("id")
ON DELETE SET NULL
ON UPDATE CASCADE;
ALTER TABLE "ownership_transfers"
ADD CONSTRAINT "ownership_transfers_requested_by_device_id_fkey"
FOREIGN KEY ("requested_by_device_id") REFERENCES "devices"("id")
ON DELETE SET NULL
ON UPDATE CASCADE;
+163 -51
View File
@@ -8,42 +8,55 @@ datasource db {
}
model User {
id String @id @default(uuid()) @db.Uuid
slug String @unique
displayName String @map("display_name")
isDefault Boolean @default(false) @map("is_default")
libraryCursor BigInt @default(0) @map("library_cursor")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
devices Device[]
tracks Track[]
audioAssets AudioAsset[]
artworkAssets ArtworkAsset[]
uploadSessions UploadSession[]
libraryEvents LibraryEvent[]
syncCursors DeviceSyncCursor[]
id String @id @default(uuid()) @db.Uuid
slug String @unique
displayName String @map("display_name")
isDefault Boolean @default(false) @map("is_default")
accountKind UserAccountKind @default(LEGACY_DEFAULT) @map("account_kind")
accountStatus UserAccountStatus @default(ACTIVE) @map("account_status")
libraryNamespace String @unique @default(uuid()) @map("library_namespace") @db.Uuid
libraryCursor BigInt @default(0) @map("library_cursor")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
devices Device[]
tracks Track[]
audioAssets AudioAsset[]
artworkAssets ArtworkAsset[]
uploadSessions UploadSession[]
libraryEvents LibraryEvent[]
syncCursors DeviceSyncCursor[]
passwordCredential UserPasswordCredential?
oauthIdentities UserOAuthIdentity[]
emailVerificationTokens EmailVerificationToken[]
passwordResetTokens PasswordResetToken[]
deviceLinkHistory DeviceLinkHistory[]
outgoingTransfers OwnershipTransfer[] @relation("OwnershipTransferFromUser")
incomingTransfers OwnershipTransfer[] @relation("OwnershipTransferToUser")
@@map("users")
}
model Device {
id String @id @default(uuid()) @db.Uuid
userId String @db.Uuid @map("user_id")
platform DevicePlatform
deviceName String @map("device_name")
appVersion String @map("app_version")
installTokenHash String @map("install_token_hash")
tokenHash String? @unique @map("token_hash")
tokenCreatedAt DateTime? @map("token_created_at")
tokenLastUsedAt DateTime? @map("token_last_used_at")
tokenRevokedAt DateTime? @map("token_revoked_at")
lastSeenAt DateTime @map("last_seen_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
uploadSessions UploadSession[]
syncCursor DeviceSyncCursor?
audioAssets AudioAsset[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
platform DevicePlatform
deviceName String @map("device_name")
appVersion String @map("app_version")
installTokenHash String @map("install_token_hash")
tokenHash String? @unique @map("token_hash")
tokenCreatedAt DateTime? @map("token_created_at")
tokenLastUsedAt DateTime? @map("token_last_used_at")
tokenRevokedAt DateTime? @map("token_revoked_at")
lastSeenAt DateTime @map("last_seen_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
uploadSessions UploadSession[]
syncCursor DeviceSyncCursor?
audioAssets AudioAsset[]
linkHistory DeviceLinkHistory[] @relation("DeviceLinkHistoryDevice")
linkedByHistory DeviceLinkHistory[] @relation("DeviceLinkHistoryLinkedByDevice")
requestedTransfers OwnershipTransfer[] @relation("OwnershipTransferRequestedByDevice")
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([userId])
@@map("devices")
@@ -51,9 +64,9 @@ model Device {
model Track {
id String @id @default(uuid()) @db.Uuid
userId String @db.Uuid @map("user_id")
primaryAudioAssetId String? @unique @db.Uuid @map("primary_audio_asset_id")
artworkAssetId String? @db.Uuid @map("artwork_asset_id")
userId String @map("user_id") @db.Uuid
primaryAudioAssetId String? @unique @map("primary_audio_asset_id") @db.Uuid
artworkAssetId String? @map("artwork_asset_id") @db.Uuid
title String
artist String
album String?
@@ -78,8 +91,8 @@ model Track {
model AudioAsset {
id String @id @default(uuid()) @db.Uuid
userId String @db.Uuid @map("user_id")
trackId String? @db.Uuid @map("track_id")
userId String @map("user_id") @db.Uuid
trackId String? @map("track_id") @db.Uuid
sha256 String
storageKey String @unique @map("storage_key")
originalFilename String @map("original_filename")
@@ -90,7 +103,7 @@ model AudioAsset {
sampleRateHz Int? @map("sample_rate_hz")
channels Int?
durationMs Int? @map("duration_ms")
sourceDeviceId String? @db.Uuid @map("source_device_id")
sourceDeviceId String? @map("source_device_id") @db.Uuid
createdAt DateTime @default(now()) @map("created_at")
track Track? @relation("TrackAudioAssets", fields: [trackId], references: [id], onDelete: SetNull, onUpdate: Cascade)
primaryForTrack Track? @relation("PrimaryAudioAsset")
@@ -103,7 +116,7 @@ model AudioAsset {
}
model ArtworkAsset {
userId String @db.Uuid @map("user_id")
userId String @map("user_id") @db.Uuid
id String @id @default(uuid()) @db.Uuid
sha256 String
storageKey String @unique @map("storage_key")
@@ -122,10 +135,10 @@ model ArtworkAsset {
model UploadSession {
id String @id @default(uuid()) @db.Uuid
userId String @db.Uuid @map("user_id")
deviceId String @db.Uuid @map("device_id")
trackId String? @db.Uuid @map("track_id")
audioAssetId String? @db.Uuid @map("audio_asset_id")
userId String @map("user_id") @db.Uuid
deviceId String @map("device_id") @db.Uuid
trackId String? @map("track_id") @db.Uuid
audioAssetId String? @map("audio_asset_id") @db.Uuid
expectedSha256 String @map("expected_sha256")
originalFilename String @map("original_filename")
expectedSizeBytes BigInt @map("expected_size_bytes")
@@ -146,34 +159,133 @@ model UploadSession {
model LibraryEvent {
id BigInt @id @default(autoincrement())
userId String @db.Uuid @map("user_id")
userId String @map("user_id") @db.Uuid
cursor BigInt
entityType EntityType @map("entity_type")
entityId String @db.Uuid @map("entity_id")
entityId String @map("entity_id") @db.Uuid
action EventAction
payload Json
payloadVersion Int @default(1) @map("payload_version")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([userId])
@@unique([userId, cursor])
@@index([userId])
@@index([userId, cursor])
@@map("library_events")
}
model DeviceSyncCursor {
deviceId String @id @db.Uuid @map("device_id")
userId String @db.Uuid @map("user_id")
cursor BigInt @default(0)
updatedAt DateTime @updatedAt @map("updated_at")
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade, onUpdate: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
deviceId String @id @map("device_id") @db.Uuid
userId String @map("user_id") @db.Uuid
cursor BigInt @default(0)
updatedAt DateTime @updatedAt @map("updated_at")
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade, onUpdate: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([userId])
@@map("device_sync_cursors")
}
model UserPasswordCredential {
id String @id @default(uuid()) @db.Uuid
userId String @unique @map("user_id") @db.Uuid
passwordHash String @map("password_hash")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@map("user_password_credentials")
}
model UserOAuthIdentity {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
provider String
providerSubject String @map("provider_subject")
email String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@unique([provider, providerSubject])
@@index([userId])
@@map("user_oauth_identities")
}
model EmailVerificationToken {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
email String
tokenHash String @unique @map("token_hash")
expiresAt DateTime @map("expires_at")
consumedAt DateTime? @map("consumed_at")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([userId])
@@map("email_verification_tokens")
}
model PasswordResetToken {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
tokenHash String @unique @map("token_hash")
expiresAt DateTime @map("expires_at")
consumedAt DateTime? @map("consumed_at")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([userId])
@@map("password_reset_tokens")
}
model DeviceLinkHistory {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
deviceId String? @map("device_id") @db.Uuid
linkedByDeviceId String? @map("linked_by_device_id") @db.Uuid
linkMethod String @map("link_method")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
device Device? @relation("DeviceLinkHistoryDevice", fields: [deviceId], references: [id], onDelete: SetNull, onUpdate: Cascade)
linkedByDevice Device? @relation("DeviceLinkHistoryLinkedByDevice", fields: [linkedByDeviceId], references: [id], onDelete: SetNull, onUpdate: Cascade)
@@index([userId])
@@index([deviceId])
@@map("device_link_history")
}
model OwnershipTransfer {
id String @id @default(uuid()) @db.Uuid
fromUserId String @map("from_user_id") @db.Uuid
toUserId String? @map("to_user_id") @db.Uuid
requestedByDeviceId String? @map("requested_by_device_id") @db.Uuid
status String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
completedAt DateTime? @map("completed_at")
fromUser User @relation("OwnershipTransferFromUser", fields: [fromUserId], references: [id], onDelete: Cascade, onUpdate: Cascade)
toUser User? @relation("OwnershipTransferToUser", fields: [toUserId], references: [id], onDelete: SetNull, onUpdate: Cascade)
requestedByDevice Device? @relation("OwnershipTransferRequestedByDevice", fields: [requestedByDeviceId], references: [id], onDelete: SetNull, onUpdate: Cascade)
@@index([fromUserId])
@@index([toUserId])
@@map("ownership_transfers")
}
enum UserAccountKind {
LEGACY_DEFAULT
GUEST
REGISTERED
}
enum UserAccountStatus {
ACTIVE
LOCKED
DELETED
}
enum DevicePlatform {
MACOS
IPHONE