init studio auth backend

This commit is contained in:
2026-07-01 17:36:34 +09:00
commit 87f6f46d3c
35 changed files with 2492 additions and 0 deletions
@@ -0,0 +1,47 @@
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'USER');
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"name" TEXT,
"role" "UserRole" NOT NULL DEFAULT 'USER',
"is_active" BOOLEAN NOT NULL DEFAULT true,
"last_login_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "refresh_tokens" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"token_hash" TEXT NOT NULL,
"expires_at" TIMESTAMP(3) NOT NULL,
"revoked_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "refresh_tokens_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "handoff_tokens" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"token_hash" TEXT NOT NULL,
"audience_app" TEXT NOT NULL,
"expires_at" TIMESTAMP(3) NOT NULL,
"used_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "handoff_tokens_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
CREATE UNIQUE INDEX "refresh_tokens_token_hash_key" ON "refresh_tokens"("token_hash");
CREATE INDEX "refresh_tokens_user_id_idx" ON "refresh_tokens"("user_id");
CREATE UNIQUE INDEX "handoff_tokens_token_hash_key" ON "handoff_tokens"("token_hash");
CREATE INDEX "handoff_tokens_user_id_idx" ON "handoff_tokens"("user_id");
ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "handoff_tokens" ADD CONSTRAINT "handoff_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,16 @@
CREATE TABLE "login_notices" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"link_url" TEXT,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"starts_at" TIMESTAMP(3),
"ends_at" TIMESTAMP(3),
"priority" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "login_notices_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "login_notices_is_active_priority_idx" ON "login_notices"("is_active", "priority");
@@ -0,0 +1,49 @@
-- CreateEnum
CREATE TYPE "OrganizationType" AS ENUM ('COMPANY', 'SCHOOL', 'GOVERNMENT', 'LAB', 'OTHER');
-- CreateTable
CREATE TABLE "organizations" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"type" "OrganizationType" NOT NULL DEFAULT 'OTHER',
"domain" TEXT,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "organizations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "user_affiliations" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"organization_id" TEXT NOT NULL,
"department" TEXT,
"position" TEXT,
"is_primary" BOOLEAN NOT NULL DEFAULT false,
"starts_at" TIMESTAMP(3),
"ends_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "user_affiliations_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "organizations_name_type_key" ON "organizations"("name", "type");
-- CreateIndex
CREATE INDEX "organizations_is_active_type_idx" ON "organizations"("is_active", "type");
-- CreateIndex
CREATE INDEX "user_affiliations_user_id_is_primary_idx" ON "user_affiliations"("user_id", "is_primary");
-- CreateIndex
CREATE INDEX "user_affiliations_organization_id_idx" ON "user_affiliations"("organization_id");
-- AddForeignKey
ALTER TABLE "user_affiliations" ADD CONSTRAINT "user_affiliations_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_affiliations" ADD CONSTRAINT "user_affiliations_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,22 @@
COMMENT ON TYPE "OrganizationType" IS '사용자 소속 유형. 회사, 학교, 공공기관, 연구소, 기타를 구분한다.';
COMMENT ON TABLE "organizations" IS '사용자에게 부여할 수 있는 소속 마스터. 회사, 학교, 기관, 연구소 등을 통합 관리한다.';
COMMENT ON COLUMN "organizations"."id" IS '소속 고유 ID';
COMMENT ON COLUMN "organizations"."name" IS '소속명';
COMMENT ON COLUMN "organizations"."type" IS '소속 유형';
COMMENT ON COLUMN "organizations"."domain" IS '소속 이메일/웹 도메인. 선택 입력값';
COMMENT ON COLUMN "organizations"."is_active" IS '소속 사용 여부';
COMMENT ON COLUMN "organizations"."created_at" IS '소속 생성 일시';
COMMENT ON COLUMN "organizations"."updated_at" IS '소속 수정 일시';
COMMENT ON TABLE "user_affiliations" IS '사용자와 소속의 연결 정보. 사용자 1명은 여러 소속을 가질 수 있다.';
COMMENT ON COLUMN "user_affiliations"."id" IS '사용자 소속 연결 고유 ID';
COMMENT ON COLUMN "user_affiliations"."user_id" IS '사용자 ID';
COMMENT ON COLUMN "user_affiliations"."organization_id" IS '소속 ID';
COMMENT ON COLUMN "user_affiliations"."department" IS '부서, 학과, 조직 단위명';
COMMENT ON COLUMN "user_affiliations"."position" IS '직책, 직급, 역할명';
COMMENT ON COLUMN "user_affiliations"."is_primary" IS '대표 소속 여부';
COMMENT ON COLUMN "user_affiliations"."starts_at" IS '소속 시작 일시';
COMMENT ON COLUMN "user_affiliations"."ends_at" IS '소속 종료 일시. NULL이면 현재 유효한 소속';
COMMENT ON COLUMN "user_affiliations"."created_at" IS '사용자 소속 연결 생성 일시';
COMMENT ON COLUMN "user_affiliations"."updated_at" IS '사용자 소속 연결 수정 일시';
@@ -0,0 +1,41 @@
COMMENT ON TYPE "UserRole" IS 'auth 사용자 권한. 관리자와 일반 사용자를 구분한다.';
COMMENT ON TABLE "users" IS '중앙 인증 사용자 계정';
COMMENT ON COLUMN "users"."id" IS '사용자 고유 ID';
COMMENT ON COLUMN "users"."email" IS '로그인 이메일';
COMMENT ON COLUMN "users"."password_hash" IS '해시 처리된 비밀번호';
COMMENT ON COLUMN "users"."name" IS '사용자 이름';
COMMENT ON COLUMN "users"."role" IS '사용자 권한';
COMMENT ON COLUMN "users"."is_active" IS '계정 활성 여부';
COMMENT ON COLUMN "users"."last_login_at" IS '마지막 로그인 일시';
COMMENT ON COLUMN "users"."created_at" IS '계정 생성 일시';
COMMENT ON COLUMN "users"."updated_at" IS '계정 수정 일시';
COMMENT ON TABLE "refresh_tokens" IS '로그인 세션 유지를 위한 refresh token 저장소';
COMMENT ON COLUMN "refresh_tokens"."id" IS 'refresh token 레코드 고유 ID';
COMMENT ON COLUMN "refresh_tokens"."user_id" IS '사용자 ID';
COMMENT ON COLUMN "refresh_tokens"."token_hash" IS 'refresh token 해시값';
COMMENT ON COLUMN "refresh_tokens"."expires_at" IS 'refresh token 만료 일시';
COMMENT ON COLUMN "refresh_tokens"."revoked_at" IS 'refresh token 폐기 일시';
COMMENT ON COLUMN "refresh_tokens"."created_at" IS 'refresh token 생성 일시';
COMMENT ON TABLE "handoff_tokens" IS '런처에서 개별 앱으로 인증 상태를 전달하기 위한 일회성 토큰';
COMMENT ON COLUMN "handoff_tokens"."id" IS 'handoff token 레코드 고유 ID';
COMMENT ON COLUMN "handoff_tokens"."user_id" IS '사용자 ID';
COMMENT ON COLUMN "handoff_tokens"."token_hash" IS 'handoff token 해시값';
COMMENT ON COLUMN "handoff_tokens"."audience_app" IS '토큰을 사용할 대상 앱 식별자';
COMMENT ON COLUMN "handoff_tokens"."expires_at" IS 'handoff token 만료 일시';
COMMENT ON COLUMN "handoff_tokens"."used_at" IS 'handoff token 사용 일시';
COMMENT ON COLUMN "handoff_tokens"."created_at" IS 'handoff token 생성 일시';
COMMENT ON TABLE "login_notices" IS '런처 로그인 화면에 노출할 공지사항';
COMMENT ON COLUMN "login_notices"."id" IS '로그인 공지 고유 ID';
COMMENT ON COLUMN "login_notices"."title" IS '공지 제목';
COMMENT ON COLUMN "login_notices"."body" IS '공지 본문';
COMMENT ON COLUMN "login_notices"."link_url" IS '자세히보기 링크 URL';
COMMENT ON COLUMN "login_notices"."is_active" IS '공지 활성 여부';
COMMENT ON COLUMN "login_notices"."starts_at" IS '공지 노출 시작 일시';
COMMENT ON COLUMN "login_notices"."ends_at" IS '공지 노출 종료 일시';
COMMENT ON COLUMN "login_notices"."priority" IS '공지 우선순위. 값이 높을수록 우선 노출';
COMMENT ON COLUMN "login_notices"."created_at" IS '공지 생성 일시';
COMMENT ON COLUMN "login_notices"."updated_at" IS '공지 수정 일시';
@@ -0,0 +1,56 @@
INSERT INTO "organizations" ("id", "name", "type", "domain", "is_active", "created_at", "updated_at")
VALUES
('11111111-1111-4111-8111-111111111111', '퓨처드라이브', 'COMPANY', 'futuredrive.net', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('22222222-2222-4222-8222-222222222222', '영남대학교', 'SCHOOL', 'yu.ac.kr', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT ("name", "type") DO UPDATE
SET
"domain" = EXCLUDED."domain",
"is_active" = true,
"updated_at" = CURRENT_TIMESTAMP;
UPDATE "user_affiliations"
SET "is_primary" = false,
"updated_at" = CURRENT_TIMESTAMP
WHERE "ends_at" IS NULL;
UPDATE "user_affiliations"
SET "is_primary" = true,
"updated_at" = CURRENT_TIMESTAMP
WHERE "organization_id" = '11111111-1111-4111-8111-111111111111'
AND "ends_at" IS NULL;
INSERT INTO "user_affiliations" (
"id",
"user_id",
"organization_id",
"department",
"position",
"is_primary",
"starts_at",
"created_at",
"updated_at"
)
SELECT
lower(
substr(md5("users"."id" || ':futuredrive'), 1, 8) || '-' ||
substr(md5("users"."id" || ':futuredrive'), 9, 4) || '-4' ||
substr(md5("users"."id" || ':futuredrive'), 14, 3) || '-8' ||
substr(md5("users"."id" || ':futuredrive'), 18, 3) || '-' ||
substr(md5("users"."id" || ':futuredrive'), 21, 12)
),
"users"."id",
'11111111-1111-4111-8111-111111111111',
NULL,
NULL,
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
FROM "users"
WHERE NOT EXISTS (
SELECT 1
FROM "user_affiliations"
WHERE "user_affiliations"."user_id" = "users"."id"
AND "user_affiliations"."organization_id" = '11111111-1111-4111-8111-111111111111'
AND "user_affiliations"."ends_at" IS NULL
);
@@ -0,0 +1,9 @@
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
UPDATE "organizations"
SET "id" = gen_random_uuid()::text,
"updated_at" = CURRENT_TIMESTAMP
WHERE "id" IN (
'11111111-1111-4111-8111-111111111111',
'22222222-2222-4222-8222-222222222222'
);
@@ -0,0 +1,57 @@
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
UPDATE "users"
SET "id" = gen_random_uuid()::text
WHERE "id" !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
UPDATE "organizations"
SET "id" = gen_random_uuid()::text
WHERE "id" !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
UPDATE "refresh_tokens"
SET "id" = gen_random_uuid()::text
WHERE "id" !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
UPDATE "handoff_tokens"
SET "id" = gen_random_uuid()::text
WHERE "id" !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
UPDATE "user_affiliations"
SET "id" = gen_random_uuid()::text
WHERE "id" !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
UPDATE "login_notices"
SET "id" = gen_random_uuid()::text
WHERE "id" !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
ALTER TABLE "refresh_tokens" DROP CONSTRAINT "refresh_tokens_user_id_fkey";
ALTER TABLE "handoff_tokens" DROP CONSTRAINT "handoff_tokens_user_id_fkey";
ALTER TABLE "user_affiliations" DROP CONSTRAINT "user_affiliations_user_id_fkey";
ALTER TABLE "user_affiliations" DROP CONSTRAINT "user_affiliations_organization_id_fkey";
ALTER TABLE "users"
ALTER COLUMN "id" TYPE uuid USING "id"::uuid;
ALTER TABLE "organizations"
ALTER COLUMN "id" TYPE uuid USING "id"::uuid;
ALTER TABLE "refresh_tokens"
ALTER COLUMN "id" TYPE uuid USING "id"::uuid,
ALTER COLUMN "user_id" TYPE uuid USING "user_id"::uuid;
ALTER TABLE "handoff_tokens"
ALTER COLUMN "id" TYPE uuid USING "id"::uuid,
ALTER COLUMN "user_id" TYPE uuid USING "user_id"::uuid;
ALTER TABLE "user_affiliations"
ALTER COLUMN "id" TYPE uuid USING "id"::uuid,
ALTER COLUMN "user_id" TYPE uuid USING "user_id"::uuid,
ALTER COLUMN "organization_id" TYPE uuid USING "organization_id"::uuid;
ALTER TABLE "login_notices"
ALTER COLUMN "id" TYPE uuid USING "id"::uuid;
ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "handoff_tokens" ADD CONSTRAINT "handoff_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "user_affiliations" ADD CONSTRAINT "user_affiliations_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "user_affiliations" ADD CONSTRAINT "user_affiliations_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,68 @@
CREATE TYPE "LicenseCode" AS ENUM ('FREE', 'EDUCATION', 'PROFESSIONAL');
CREATE TABLE "licenses" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"code" "LicenseCode" NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "licenses_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "user_licenses" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"user_id" UUID NOT NULL,
"license_id" UUID NOT NULL,
"starts_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" TIMESTAMP(3),
"assigned_by" UUID,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "user_licenses_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "licenses_code_key" ON "licenses"("code");
CREATE INDEX "licenses_is_active_idx" ON "licenses"("is_active");
CREATE INDEX "user_licenses_user_id_starts_at_idx" ON "user_licenses"("user_id", "starts_at");
CREATE INDEX "user_licenses_license_id_idx" ON "user_licenses"("license_id");
ALTER TABLE "user_licenses" ADD CONSTRAINT "user_licenses_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "user_licenses" ADD CONSTRAINT "user_licenses_license_id_fkey" FOREIGN KEY ("license_id") REFERENCES "licenses"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
INSERT INTO "licenses" ("code", "name", "description", "updated_at")
VALUES
('FREE', 'Free', '기본 무료 라이선스', CURRENT_TIMESTAMP),
('EDUCATION', 'Education', '교육기관용 라이선스', CURRENT_TIMESTAMP),
('PROFESSIONAL', 'Professional', '전문가용 라이선스', CURRENT_TIMESTAMP);
INSERT INTO "user_licenses" ("user_id", "license_id", "starts_at")
SELECT u."id", l."id", CURRENT_TIMESTAMP
FROM "users" u
CROSS JOIN "licenses" l
WHERE l."code" = 'PROFESSIONAL'
AND NOT EXISTS (
SELECT 1
FROM "user_licenses" ul
WHERE ul."user_id" = u."id"
);
COMMENT ON TYPE "LicenseCode" IS '라이선스 코드. 무료, 교육용, 전문가용 플랜을 구분한다.';
COMMENT ON TABLE "licenses" IS 'Autonomy Studio 라이선스 마스터. 플랜 코드와 표시명을 관리한다.';
COMMENT ON COLUMN "licenses"."id" IS '라이선스 UUID.';
COMMENT ON COLUMN "licenses"."code" IS '라이선스 코드.';
COMMENT ON COLUMN "licenses"."name" IS '화면에 표시할 라이선스명.';
COMMENT ON COLUMN "licenses"."description" IS '라이선스 설명.';
COMMENT ON COLUMN "licenses"."is_active" IS '라이선스 사용 가능 여부.';
COMMENT ON COLUMN "licenses"."created_at" IS '라이선스 생성 시각.';
COMMENT ON COLUMN "licenses"."updated_at" IS '라이선스 수정 시각.';
COMMENT ON TABLE "user_licenses" IS '사용자별 라이선스 부여 이력. 현재 유효 라이선스와 기간을 관리한다.';
COMMENT ON COLUMN "user_licenses"."id" IS '사용자 라이선스 UUID.';
COMMENT ON COLUMN "user_licenses"."user_id" IS '라이선스를 부여받은 사용자 UUID.';
COMMENT ON COLUMN "user_licenses"."license_id" IS '부여된 라이선스 UUID.';
COMMENT ON COLUMN "user_licenses"."starts_at" IS '라이선스 시작 시각.';
COMMENT ON COLUMN "user_licenses"."expires_at" IS '라이선스 만료 시각. NULL이면 무기한.';
COMMENT ON COLUMN "user_licenses"."assigned_by" IS '라이선스를 부여한 관리자 UUID.';
COMMENT ON COLUMN "user_licenses"."created_at" IS '라이선스 부여 이력 생성 시각.';
@@ -0,0 +1,8 @@
ALTER TABLE "users"
ADD COLUMN "profile_image_data" BYTEA,
ADD COLUMN "profile_image_mime" TEXT,
ADD COLUMN "profile_image_updated_at" TIMESTAMP(3);
COMMENT ON COLUMN "users"."profile_image_data" IS '사용자 프로필 썸네일 이미지 바이너리. 128px WebP 압축본만 저장한다.';
COMMENT ON COLUMN "users"."profile_image_mime" IS '프로필 썸네일 MIME 타입.';
COMMENT ON COLUMN "users"."profile_image_updated_at" IS '프로필 썸네일 마지막 수정 시각.';
@@ -0,0 +1,19 @@
DO $$
DECLARE
column_record record;
BEGIN
FOR column_record IN
SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND data_type = 'timestamp without time zone'
LOOP
EXECUTE format(
'ALTER TABLE %I.%I ALTER COLUMN %I TYPE timestamptz USING %I AT TIME ZONE ''UTC''',
column_record.table_schema,
column_record.table_name,
column_record.column_name,
column_record.column_name
);
END LOOP;
END $$;
@@ -0,0 +1,12 @@
ALTER TABLE "licenses"
ADD COLUMN "default_duration_days" INTEGER DEFAULT 365;
UPDATE "licenses"
SET "default_duration_days" = 365
WHERE "default_duration_days" IS NULL;
UPDATE "user_licenses"
SET "expires_at" = "starts_at" + INTERVAL '365 days'
WHERE "expires_at" IS NULL;
COMMENT ON COLUMN "licenses"."default_duration_days" IS '사용자에게 라이선스를 부여할 때 기본 적용할 기간(일). NULL이면 무기한.';
@@ -0,0 +1,40 @@
CREATE TABLE "user_app_roles" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"user_id" uuid NOT NULL,
"app_key" text NOT NULL,
"role_code" text NOT NULL,
"assigned_by" uuid,
"assigned_at" timestamptz(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"starts_at" timestamptz(3),
"ends_at" timestamptz(3),
"created_at" timestamptz(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamptz(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "user_app_roles_pkey" PRIMARY KEY ("id"),
CONSTRAINT "user_app_roles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE UNIQUE INDEX "user_app_roles_user_id_app_key_role_code_key" ON "user_app_roles"("user_id", "app_key", "role_code");
CREATE INDEX "user_app_roles_app_key_role_code_idx" ON "user_app_roles"("app_key", "role_code");
CREATE INDEX "user_app_roles_user_id_app_key_idx" ON "user_app_roles"("user_id", "app_key");
COMMENT ON TABLE "user_app_roles" IS '사용자별 앱 역할. 전역 계정 역할과 별도로 LMS 강사, 학생, ADS IDE 개발자 같은 앱 단위 역할을 관리한다.';
COMMENT ON COLUMN "user_app_roles"."id" IS '앱 역할 매핑 UUID';
COMMENT ON COLUMN "user_app_roles"."user_id" IS '역할을 부여받은 auth 사용자 ID';
COMMENT ON COLUMN "user_app_roles"."app_key" IS '역할이 적용되는 앱 식별자. 예: LMS, ADS IDE';
COMMENT ON COLUMN "user_app_roles"."role_code" IS '앱 내부 역할 코드. 예: INSTRUCTOR, STUDENT, DEVELOPER';
COMMENT ON COLUMN "user_app_roles"."assigned_by" IS '역할을 부여한 관리자 사용자 ID';
COMMENT ON COLUMN "user_app_roles"."assigned_at" IS '역할 부여 일시';
COMMENT ON COLUMN "user_app_roles"."starts_at" IS '역할 유효 시작 일시. NULL이면 즉시 유효';
COMMENT ON COLUMN "user_app_roles"."ends_at" IS '역할 유효 종료 일시. NULL이면 무기한';
COMMENT ON COLUMN "user_app_roles"."created_at" IS '생성 일시';
COMMENT ON COLUMN "user_app_roles"."updated_at" IS '수정 일시';
INSERT INTO "user_app_roles" ("user_id", "app_key", "role_code")
SELECT "id", 'LMS', 'INSTRUCTOR'
FROM "users"
WHERE "email" IN ('teacher@futuredrive.net', 'teacher@yu.ac.kr')
ON CONFLICT ("user_id", "app_key", "role_code") DO NOTHING;
UPDATE "users"
SET "role" = 'USER'
WHERE "email" IN ('teacher@futuredrive.net', 'teacher@yu.ac.kr');
@@ -0,0 +1,2 @@
ALTER TYPE "LicenseCode" RENAME VALUE 'EDUCATION' TO 'EDU';
ALTER TYPE "LicenseCode" RENAME VALUE 'PROFESSIONAL' TO 'PRO';
+1
View File
@@ -0,0 +1 @@
provider = "postgresql"