feat: 관리자 계정 식별자와 카카오 영구 교체 지원
관리자 승인과 사용자 OAuth 증명을 분리하고 기존 Kakao stable ID를 영구 폐기한다. 로그인 ID와 닉네임 변경은 현재 장수에 revision 기반으로 투영하며 과거 기록은 당시 이름으로 보존한다.
This commit is contained in:
@@ -24,6 +24,7 @@ export interface GatewayUserInfo {
|
||||
id: string;
|
||||
username: string;
|
||||
displayName: string;
|
||||
identityRevision?: string;
|
||||
roles: string[];
|
||||
picture?: string;
|
||||
imageServer?: number;
|
||||
@@ -102,6 +103,8 @@ export const parseGameSessionTokenPayload = (value: unknown): GameSessionTokenPa
|
||||
typeof user.id !== 'string' ||
|
||||
typeof user.username !== 'string' ||
|
||||
typeof user.displayName !== 'string' ||
|
||||
(user.identityRevision !== undefined &&
|
||||
(typeof user.identityRevision !== 'string' || !isCanonicalIsoTimestamp(user.identityRevision))) ||
|
||||
!Array.isArray(user.roles) ||
|
||||
(user.picture !== undefined && typeof user.picture !== 'string') ||
|
||||
(user.imageServer !== undefined && (!Number.isSafeInteger(user.imageServer) || user.imageServer < 0)) ||
|
||||
|
||||
@@ -348,6 +348,13 @@ export type TurnDaemonCommand =
|
||||
iconRevision: string;
|
||||
enforceCooldown?: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'adjustGeneralIdentity';
|
||||
requestId?: string;
|
||||
userId: string;
|
||||
displayName: string;
|
||||
identityRevision: string;
|
||||
}
|
||||
| {
|
||||
type: 'joinCreateGeneral';
|
||||
requestId?: string;
|
||||
@@ -757,6 +764,18 @@ export type TurnDaemonCommandResult =
|
||||
reason: string;
|
||||
availableAt?: string;
|
||||
}
|
||||
| {
|
||||
type: 'adjustGeneralIdentity';
|
||||
ok: true;
|
||||
generalId: number | null;
|
||||
updated: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'adjustGeneralIdentity';
|
||||
ok: false;
|
||||
code: 'CONFLICT' | 'PRECONDITION_FAILED';
|
||||
reason: string;
|
||||
}
|
||||
| {
|
||||
type: 'joinCreateGeneral';
|
||||
ok: true;
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
ALTER TABLE "app_user"
|
||||
ADD COLUMN "identity_revision" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "auth_revision" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "session_revoked_before" TIMESTAMP(3),
|
||||
ADD COLUMN "kakao_replacement_approved_until" TIMESTAMP(3),
|
||||
ADD COLUMN "kakao_replacement_approved_by_user_id" TEXT,
|
||||
ADD COLUMN "kakao_replacement_reason" TEXT;
|
||||
|
||||
CREATE TABLE "retired_kakao_identity" (
|
||||
"id" TEXT NOT NULL,
|
||||
"oauth_id" TEXT NOT NULL,
|
||||
"former_user_id" TEXT NOT NULL,
|
||||
"approved_by_user_id" TEXT NOT NULL,
|
||||
"reason" TEXT NOT NULL,
|
||||
"retired_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "retired_kakao_identity_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "retired_kakao_identity_oauth_id_key"
|
||||
ON "retired_kakao_identity"("oauth_id");
|
||||
|
||||
CREATE INDEX "retired_kakao_identity_former_user_id_retired_at_idx"
|
||||
ON "retired_kakao_identity"("former_user_id", "retired_at");
|
||||
@@ -79,45 +79,63 @@ enum GatewaySourceMode {
|
||||
}
|
||||
|
||||
model AppUser {
|
||||
id String @id @default(uuid())
|
||||
loginId String @unique @map("login_id")
|
||||
displayName String @unique @map("display_name")
|
||||
passwordHash String @map("password_hash")
|
||||
passwordSalt String @map("password_salt")
|
||||
passwordResetRequired Boolean @default(false) @map("password_reset_required")
|
||||
roles Json @default(dbgenerated("'[]'::jsonb"))
|
||||
sanctions Json @default(dbgenerated("'{}'::jsonb"))
|
||||
oauthType OAuthType @default(NONE) @map("oauth_type")
|
||||
oauthId String? @unique @map("oauth_id")
|
||||
email String? @unique
|
||||
oauthInfo Json @default(dbgenerated("'{}'::jsonb")) @map("oauth_info")
|
||||
picture String @default("default.jpg")
|
||||
imageServer Int @default(0) @map("image_server")
|
||||
iconUpdatedAt DateTime? @map("icon_updated_at")
|
||||
iconRevision DateTime? @map("icon_revision")
|
||||
profileIconResetAt DateTime? @map("profile_icon_reset_at")
|
||||
iconRetiredAt DateTime? @map("icon_retired_at")
|
||||
thirdPartyUse Boolean @default(true) @map("third_party_use")
|
||||
termsAcceptedAt DateTime? @map("terms_accepted_at")
|
||||
privacyAcceptedAt DateTime? @map("privacy_accepted_at")
|
||||
kakaoVerifiedAt DateTime? @map("kakao_verified_at")
|
||||
kakaoTalkVerifiedUntil DateTime? @map("kakao_talk_verified_until")
|
||||
kakaoGraceStartedAt DateTime @default(now()) @map("kakao_grace_started_at")
|
||||
kakaoGraceUntil DateTime? @map("kakao_grace_until")
|
||||
deleteAfter DateTime? @map("delete_after")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
lastLoginAt DateTime? @map("last_login_at")
|
||||
legacyData Json @default(dbgenerated("'{}'::jsonb")) @map("legacy_data")
|
||||
icons UserIcon[]
|
||||
specialAccessGrants SpecialAccountAccessGrant[]
|
||||
webPushSubscriptions WebPushSubscription[]
|
||||
webPushPreferences WebPushPreference[]
|
||||
webPushNotifications WebPushNotification[]
|
||||
id String @id @default(uuid())
|
||||
loginId String @unique @map("login_id")
|
||||
displayName String @unique @map("display_name")
|
||||
passwordHash String @map("password_hash")
|
||||
passwordSalt String @map("password_salt")
|
||||
passwordResetRequired Boolean @default(false) @map("password_reset_required")
|
||||
roles Json @default(dbgenerated("'[]'::jsonb"))
|
||||
sanctions Json @default(dbgenerated("'{}'::jsonb"))
|
||||
oauthType OAuthType @default(NONE) @map("oauth_type")
|
||||
oauthId String? @unique @map("oauth_id")
|
||||
email String? @unique
|
||||
oauthInfo Json @default(dbgenerated("'{}'::jsonb")) @map("oauth_info")
|
||||
identityRevision DateTime @default(now()) @map("identity_revision")
|
||||
authRevision Int @default(0) @map("auth_revision")
|
||||
sessionRevokedBefore DateTime? @map("session_revoked_before")
|
||||
kakaoReplacementApprovedUntil DateTime? @map("kakao_replacement_approved_until")
|
||||
kakaoReplacementApprovedByUserId String? @map("kakao_replacement_approved_by_user_id")
|
||||
kakaoReplacementReason String? @map("kakao_replacement_reason")
|
||||
picture String @default("default.jpg")
|
||||
imageServer Int @default(0) @map("image_server")
|
||||
iconUpdatedAt DateTime? @map("icon_updated_at")
|
||||
iconRevision DateTime? @map("icon_revision")
|
||||
profileIconResetAt DateTime? @map("profile_icon_reset_at")
|
||||
iconRetiredAt DateTime? @map("icon_retired_at")
|
||||
thirdPartyUse Boolean @default(true) @map("third_party_use")
|
||||
termsAcceptedAt DateTime? @map("terms_accepted_at")
|
||||
privacyAcceptedAt DateTime? @map("privacy_accepted_at")
|
||||
kakaoVerifiedAt DateTime? @map("kakao_verified_at")
|
||||
kakaoTalkVerifiedUntil DateTime? @map("kakao_talk_verified_until")
|
||||
kakaoGraceStartedAt DateTime @default(now()) @map("kakao_grace_started_at")
|
||||
kakaoGraceUntil DateTime? @map("kakao_grace_until")
|
||||
deleteAfter DateTime? @map("delete_after")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
lastLoginAt DateTime? @map("last_login_at")
|
||||
legacyData Json @default(dbgenerated("'{}'::jsonb")) @map("legacy_data")
|
||||
icons UserIcon[]
|
||||
specialAccessGrants SpecialAccountAccessGrant[]
|
||||
webPushSubscriptions WebPushSubscription[]
|
||||
webPushPreferences WebPushPreference[]
|
||||
webPushNotifications WebPushNotification[]
|
||||
|
||||
@@map("app_user")
|
||||
}
|
||||
|
||||
model RetiredKakaoIdentity {
|
||||
id String @id @default(uuid())
|
||||
oauthId String @unique @map("oauth_id")
|
||||
formerUserId String @map("former_user_id")
|
||||
approvedByUserId String @map("approved_by_user_id")
|
||||
reason String
|
||||
retiredAt DateTime @default(now()) @map("retired_at")
|
||||
|
||||
@@index([formerUserId, retiredAt])
|
||||
@@map("retired_kakao_identity")
|
||||
}
|
||||
|
||||
model SpecialAccountAccessGrant {
|
||||
id String @id @default(uuid())
|
||||
userId String @map("user_id")
|
||||
@@ -244,18 +262,18 @@ model GatewayProfile {
|
||||
}
|
||||
|
||||
model WebPushSubscription {
|
||||
id String @id @default(uuid())
|
||||
userId String @map("user_id")
|
||||
user AppUser @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
endpoint String @unique @db.Text
|
||||
p256dh String @db.Text
|
||||
auth String @db.Text
|
||||
expirationTime DateTime? @map("expiration_time")
|
||||
userAgent String? @map("user_agent") @db.Text
|
||||
disabledAt DateTime? @map("disabled_at")
|
||||
lastSeenAt DateTime @default(now()) @map("last_seen_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
id String @id @default(uuid())
|
||||
userId String @map("user_id")
|
||||
user AppUser @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
endpoint String @unique @db.Text
|
||||
p256dh String @db.Text
|
||||
auth String @db.Text
|
||||
expirationTime DateTime? @map("expiration_time")
|
||||
userAgent String? @map("user_agent") @db.Text
|
||||
disabledAt DateTime? @map("disabled_at")
|
||||
lastSeenAt DateTime @default(now()) @map("last_seen_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deliveries WebPushDelivery[]
|
||||
|
||||
@@index([userId, disabledAt, updatedAt])
|
||||
@@ -328,11 +346,11 @@ model WebPushDelivery {
|
||||
}
|
||||
|
||||
model WebPushProfileCursor {
|
||||
profileName String @id @map("profile_name")
|
||||
profileName String @id @map("profile_name")
|
||||
status String
|
||||
preopenAt DateTime? @map("preopen_at")
|
||||
openAt DateTime? @map("open_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("web_push_profile_cursor")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user