From fba3075ba60a070abd1794a6b68f47af398a8f57 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 11:13:04 +0900 Subject: [PATCH 01/26] =?UTF-8?q?fix(security):=20=EB=B3=B4=EC=95=88=20Med?= =?UTF-8?q?ium=205=EA=B1=B4=20=EC=9D=BC=EA=B4=84=20=ED=95=B4=EC=86=8C=20?= =?UTF-8?q?=E2=80=94=20=ED=83=80=EC=9D=B4=EB=B0=8D=20=EC=97=B4=EA=B1=B0?= =?UTF-8?q?=C2=B7=EA=B3=84=EC=A0=95=20=EC=9E=A0=EA=B8=88=C2=B7tenant=20?= =?UTF-8?q?=EA=B2=A9=EB=A6=AC=C2=B7code=20=EC=84=B8=EC=85=98=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=C2=B7issuer=20fail-closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그인 타이밍 균등화: 미존재/비활성/무크레덴셜 경로에도 동일 비용 더미 scrypt 수행 (사용자 열거 오라클 제거) - 계정 단위 잠금: login:user: 키 슬라이딩 윈도우(10회/15분, 실패만 카운트, IP 제한과 병행) - users lookup API: ?id= 경로에도 tenant 스코프 강제 (?tenant= 미지정 시 default 폴백으로 무회귀) - authorization_code 교환 시 grant.sessionId 세션의 revoked/expired 검사 (invalid_grant) - 프로덕션에서 IDP_ISSUER_URL 미설정 시 host fallback 대신 503 fail-closed, 부트스트랩 필수 env fail-fast - i18n ko/en 키 확장(후속 커밋의 email/admin 키 포함 일괄 반영) Co-Authored-By: Claude Fable 5 --- .env.example | 5 +- README.md | 20 +-- src/lib/i18n/en.json | 159 +++++++++++++++++++++++- src/lib/i18n/ko.json | 159 +++++++++++++++++++++++- src/lib/server/auth/bootstrap.ts | 33 ++++- src/lib/server/auth/runtime.ts | 24 ++-- src/lib/server/auth/users.ts | 15 +++ src/routes/(auth)/login/+page.server.ts | 63 +++++++++- src/routes/api/users/lookup/+server.ts | 20 ++- src/routes/oidc/token/+server.ts | 40 +++++- 10 files changed, 504 insertions(+), 34 deletions(-) diff --git a/.env.example b/.env.example index df41d0f..c6593ee 100644 --- a/.env.example +++ b/.env.example @@ -71,11 +71,14 @@ IDP_DEFAULT_TENANT_NAME="My Organization" # 서명 키 암호화 KEK — 최소 32자 랜덤 문자열 # 생성: openssl rand -base64 32 +# ⚠️ 프로덕션 필수: 미설정 시 요청 초기에 오류로 차단(fail-fast). dev 에서는 생략 가능. # ⚠️ 프로덕션에서는 wrangler secret put IDP_SIGNING_KEY_SECRET 사용 IDP_SIGNING_KEY_SECRET="your-very-long-random-secret-at-least-32-chars" # OIDC/SAML issuer URL (배포 도메인과 일치시킬 것) -# 미설정 시 요청 origin으로 자동 대체 (로컬 개발 시 http://localhost:5173) +# ⚠️ 프로덕션 필수: 미설정 시 요청 초기에 503 으로 차단(fail-closed). Host 헤더 주입으로 +# iss 클레임/SAML Issuer 가 오염되는 것을 막는다. +# dev 에서만 미설정 시 요청 origin 으로 자동 대체(로컬 http://localhost:5173). IDP_ISSUER_URL="http://localhost:5173" SMTP_HOSTNAME="" diff --git a/README.md b/README.md index c52fd1a..9254c2a 100644 --- a/README.md +++ b/README.md @@ -197,16 +197,16 @@ wrangler secret put IDP_SIGNING_KEY_SECRET ## 환경변수 -| 변수 | 필수 | 설명 | -| ----------------------------------- | ---- | ---------------------------------------------------------------------------------------- | -| `IDP_ISSUER_URL` | ✅ | OIDC/SAML 발급자 URL (배포 도메인과 일치) | -| `IDP_SIGNING_KEY_SECRET` | ✅ | 서명 키 암호화 KEK (프로덕션은 반드시 Secret) | -| `DISPATCHER_SERVICE_TOKEN` | 선택 | stardust dispatcher 가 `/api/totp/*` 호출 시 사용할 Bearer 토큰. 미설정이면 해당 API 503 | -| `IDP_DEFAULT_TENANT_NAME` | 선택 | 기본 테넌트 이름 (기본: `My Organization`) | -| `CLOUDFLARE_ACCOUNT_ID` | 선택 | Cloudflare 계정 ID (마이그레이션 스크립트에서 사용) | -| `CLOUDFLARE_D1_DATABASE_ID` | 선택 | D1 데이터베이스 ID (마이그레이션 스크립트에서 사용) | -| `CLOUDFLARE_D1_PREVIEW_DATABASE_ID` | 선택 | 프리뷰용 D1 데이터베이스 ID | -| `CLOUDFLARE_D1_TOKEN` | 선택 | D1 API 토큰 (`db:migrate` 스크립트에서 사용) | +| 변수 | 필수 | 설명 | +| ----------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `IDP_ISSUER_URL` | ✅ | OIDC/SAML 발급자 URL (배포 도메인과 일치). **프로덕션 필수** — 미설정 시 요청 초기 503(fail-closed). dev 에서만 요청 origin 자동 대체 | +| `IDP_SIGNING_KEY_SECRET` | ✅ | 서명 키 암호화 KEK (프로덕션은 반드시 Secret). **프로덕션 필수** — 미설정 시 요청 초기에 오류로 차단(fail-fast) | +| `DISPATCHER_SERVICE_TOKEN` | 선택 | stardust dispatcher 가 `/api/totp/*` 호출 시 사용할 Bearer 토큰. 미설정이면 해당 API 503 | +| `IDP_DEFAULT_TENANT_NAME` | 선택 | 기본 테넌트 이름 (기본: `My Organization`) | +| `CLOUDFLARE_ACCOUNT_ID` | 선택 | Cloudflare 계정 ID (마이그레이션 스크립트에서 사용) | +| `CLOUDFLARE_D1_DATABASE_ID` | 선택 | D1 데이터베이스 ID (마이그레이션 스크립트에서 사용) | +| `CLOUDFLARE_D1_PREVIEW_DATABASE_ID` | 선택 | 프리뷰용 D1 데이터베이스 ID | +| `CLOUDFLARE_D1_TOKEN` | 선택 | D1 API 토큰 (`db:migrate` 스크립트에서 사용) | > **참고**: 초기 관리자 계정은 `bun run setup` 이 생성합니다. 수동/CI 시드가 필요하면 `IDP_BOOTSTRAP_ADMIN_USERNAME` / `IDP_BOOTSTRAP_ADMIN_EMAIL` / `IDP_BOOTSTRAP_ADMIN_PASSWORD` (+선택 `IDP_BOOTSTRAP_ADMIN_NAME`) 를 설정하고 `bun run db:seed`(방언별: `db:seed:pg` 등)를 실행하세요. 비대화 환경에서는 `SEED_RESET=0|1` 로 초기화 여부를 지정합니다. diff --git a/src/lib/i18n/en.json b/src/lib/i18n/en.json index d81d5ad..65b3e57 100644 --- a/src/lib/i18n/en.json +++ b/src/lib/i18n/en.json @@ -53,6 +53,7 @@ "password_reset_success": "Your password has been changed. Please log in with your new password.", "err_missing_credentials": "Please enter your username and password.", "err_rate_limit": "Too many login attempts. Please try again in {{minutes}} minutes.", + "err_account_locked": "Too many login attempts. Please try again in {{minutes}} minutes.", "err_invalid_credentials": "The username or password is incorrect.", "err_mfa_config": "There is a problem with the two-factor authentication configuration. Please contact your administrator." }, @@ -73,7 +74,90 @@ "teams": "Teams", "parts": "Parts", "ldap_providers": "LDAP Providers", - "skins": "Login Skins" + "skins": "Login Skins", + "errors": { + "invalid_request": "Invalid request.", + "user_not_found": "User not found.", + "name_required": "Name is required.", + "password_min_length": "Password must be at least 8 characters.", + "csrf_failed": "CSRF validation failed. Please refresh the page and try again.", + "invalid_expiry_format": "Invalid expiry date format.", + "required_field_missing": "Required fields are missing.", + "signing_key_secret_missing": "IDP_SIGNING_KEY_SECRET is not configured.", + "email_password_required": "Email and password are required.", + "invalid_role": "Invalid role.", + "email_taken": "This email is already in use.", + "username_taken": "This username is already in use.", + "cannot_change_own_status": "You cannot change your own status.", + "cannot_change_own_role": "You cannot change your own role.", + "password_required": "Please enter a password.", + "cannot_delete_self": "You cannot delete yourself.", + "invalid_role_value": "Invalid role value.", + "invalid_status_value": "Invalid status value.", + "cannot_change_own_role_status": "You cannot change your own role/status. Please ask another administrator.", + "last_admin_race": "Another administrator appears to have been demoted at the same time. The change was cancelled. Please try again.", + "select_department": "Please select a department.", + "department_not_found": "Department not found.", + "position_not_found": "Position not found.", + "select_team": "Please select a team.", + "team_not_found": "Team not found.", + "select_part": "Please select a part.", + "part_not_found": "Part not found.", + "select_service": "Please select a service.", + "invalid_service_type": "Invalid service type.", + "invalid_service_id": "Invalid service ID.", + "oidc_client_not_found": "OIDC client not found.", + "saml_sp_not_found": "SAML SP not found.", + "role_not_in_service": "The selected role does not belong to this service.", + "attributes_not_object": "attributesJson must be a JSON object.", + "attributes_parse_failed": "Failed to parse attributesJson.", + "assignment_exists": "A mapping already exists for this service. Please delete it first.", + "scope_openid_required": "scope must include 'openid'.", + "invalid_token_auth_method": "Invalid tokenEndpointAuthMethod value.", + "redirect_uri_required": "Redirect URI is required.", + "client_not_found": "Client not found.", + "invalid_role_key": "key allows only alphanumerics and ._- (1-64 chars).", + "label_required": "label is required.", + "role_key_exists": "This role key already exists.", + "ldap_host_required": "LDAP host is required.", + "ldap_bind_or_userdn_required": "Either Admin Bind DN or user DN pattern is required.", + "ldap_signing_key_secret_required": "IDP_SIGNING_KEY_SECRET must be configured to store the LDAP bindPassword.", + "invalid_url": "Please enter a valid URL.", + "https_only": "Only https URLs are allowed.", + "loopback_forbidden": "Loopback addresses are not allowed.", + "internal_addr_forbidden": "Internal/metadata addresses are not allowed.", + "required_fields": "Please fill in the required fields.", + "invalid_client_type": "Invalid clientType.", + "cache_ttl_negative": "cacheTtlSeconds must be 0 or greater.", + "cache_ttl_max": "cacheTtlSeconds cannot exceed {{max}} seconds (1 day).", + "skin_config_exists": "A configuration for the same client/skin type already exists.", + "skin_not_found": "Skin not found.", + "url_required": "Please enter a URL.", + "rotate_conflict": "Another rotate operation is in progress or a DB error occurred. Please try again shortly.", + "entity_id_required": "Entity ID is required.", + "acs_url_required": "ACS URL is required.", + "sp_cert_required_for_encryption": "An SP certificate is required to use assertion encryption.", + "nameid_format_forbidden": "This NameID Format is not allowed.", + "entity_id_taken": "This Entity ID is already registered.", + "login_missing_credentials": "Please enter your username and password.", + "login_invalid_credentials": "Invalid username or password.", + "login_not_admin": "This account does not have administrator privileges.", + "login_mfa_config": "MFA configuration error: IDP_SIGNING_KEY_SECRET is not configured.", + "login_mfa_required": "Administrator accounts must have MFA (OTP) configured. Please register MFA first.", + "login_rate_limit": "Too many login attempts. Please try again in {{minutes}} minute(s).", + "team_name_required": "Please enter a team name.", + "part_name_required": "Please enter a part name.", + "position_name_required": "Please enter a position name.", + "department_name_required": "Please enter a department name.", + "level_must_be_number": "Level must be a number.", + "url_invalid_format": "Invalid URL format: {{value}}", + "url_fragment_forbidden": "URIs containing a fragment (#) are not allowed: {{value}}", + "scheme_forbidden": "{{scheme}}: scheme is not allowed.", + "http_localhost_only": "http URLs are only allowed for localhost/127.0.0.1: {{value}}", + "custom_scheme_invalid": "Invalid custom scheme format: {{value}}", + "scheme_not_allowed": "Scheme is not allowed ({{scheme}}): {{value}}", + "scope_not_allowed": "Scope is not allowed: {{scope}}" + } }, "admin_login": { "subtitle": "Admin sign-in", @@ -157,7 +241,11 @@ "locale_settings": "Regional Settings", "org_membership": "Organization Membership", "team_label": "Team", - "part_label": "Part" + "part_label": "Part", + "email_unverified_title": "Your email is not verified yet.", + "email_unverified_desc": "We sent a verification email to {{email}}. Please check your inbox.", + "email_resend": "Resend verification email", + "email_resend_sent": "Verification email resent. Please check your inbox shortly." }, "audit": { "title": "Audit Log", @@ -274,6 +362,11 @@ }, "user_detail": { "back": "← List", + "danger_zone": "Danger Zone", + "force_logout": "Force logout", + "force_logout_desc": "Immediately revokes all of this user's sessions and OIDC refresh tokens. Status and password are not changed.", + "force_logout_confirm": "Force this user to log out on all devices?", + "force_logout_done": "All sessions have been revoked.", "saved": "Saved.", "profile_section": "Profile Information", "given_name": "Given Name", @@ -467,6 +560,68 @@ "submit": "Change Password", "invalid_link": "This link has expired or is invalid." }, + "verify_email": { + "title": "Email Verification", + "subtitle": "Please complete verification of your email address.", + "confirm_prompt": "Click the button below to complete verification of your email address.", + "submit": "Verify email", + "success": "Your email address has been verified.", + "invalid_link": "This link has expired or is invalid.", + "go_login": "Go to login" + }, + "errors": { + "rate_limit": "Too many requests. Please try again in {{minutes}} minute(s).", + "db_not_ready": "The D1 binding \"DB\" is not ready. Please run in a Wrangler preview/dev environment." + }, + "security_alert": { + "when": "Occurred at: {{time}}", + "footer": "If this wasn't you, change your password immediately and contact your administrator.", + "password_changed": { + "subject": "[Security Alert] Your password was changed", + "heading": "Your password was changed", + "body": "The password for your account was just changed." + }, + "password_reset_by_admin": { + "subject": "[Security Alert] An administrator reset your password", + "heading": "Your password was reset", + "body": "An administrator reset the password for your account. Please sign in with the new password." + }, + "account_locked": { + "subject": "[Security Alert] Your account was locked", + "heading": "Your account was locked", + "body": "Your account was locked by an administrator. You cannot sign in." + }, + "account_disabled": { + "subject": "[Security Alert] Your account was disabled", + "heading": "Your account was disabled", + "body": "Your account was disabled by an administrator. You cannot sign in." + }, + "mfa_enrolled": { + "subject": "[Security Alert] Two-factor authentication was enabled", + "heading": "Two-factor authentication was enabled", + "body": "Two-factor authentication (TOTP) was newly enabled on your account." + }, + "mfa_disabled": { + "subject": "[Security Alert] Two-factor authentication was disabled", + "heading": "Two-factor authentication was disabled", + "body": "Two-factor authentication (TOTP) was removed from your account." + }, + "backup_codes_regenerated": { + "subject": "[Security Alert] Backup codes were regenerated", + "heading": "Backup codes were regenerated", + "body": "New two-factor backup codes were generated for your account. Your previous backup codes no longer work." + }, + "passkey_added": { + "subject": "[Security Alert] A new passkey was registered", + "heading": "A new passkey was registered", + "body": "A new passkey was registered on your account." + }, + "passkey_removed": { + "subject": "[Security Alert] A passkey was removed", + "heading": "A passkey was removed", + "body": "A passkey was removed from your account." + } + }, "skins": { "title": "Login Skins", "add_btn": "+ Add Skin", diff --git a/src/lib/i18n/ko.json b/src/lib/i18n/ko.json index de9fc85..91c1971 100644 --- a/src/lib/i18n/ko.json +++ b/src/lib/i18n/ko.json @@ -53,6 +53,7 @@ "password_reset_success": "비밀번호가 변경되었습니다. 새 비밀번호로 로그인해 주세요.", "err_missing_credentials": "아이디와 비밀번호를 입력해 주세요.", "err_rate_limit": "로그인 시도가 너무 많습니다. {{minutes}}분 후 다시 시도해 주세요.", + "err_account_locked": "로그인 시도가 너무 많습니다. {{minutes}}분 후 다시 시도해 주세요.", "err_invalid_credentials": "아이디 또는 비밀번호가 올바르지 않습니다.", "err_mfa_config": "2단계 인증 설정에 문제가 있습니다. 관리자에게 문의해 주세요." }, @@ -73,7 +74,90 @@ "teams": "팀", "parts": "파트", "ldap_providers": "LDAP 프로바이더", - "skins": "로그인 스킨" + "skins": "로그인 스킨", + "errors": { + "invalid_request": "잘못된 요청입니다.", + "user_not_found": "사용자를 찾을 수 없습니다.", + "name_required": "이름은 필수입니다.", + "password_min_length": "비밀번호는 8자 이상이어야 합니다.", + "csrf_failed": "CSRF 검증에 실패했습니다. 페이지를 새로 고친 뒤 다시 시도해 주세요.", + "invalid_expiry_format": "만료일 형식이 올바르지 않습니다.", + "required_field_missing": "필수 항목이 누락되었습니다.", + "signing_key_secret_missing": "IDP_SIGNING_KEY_SECRET 이 설정되지 않았습니다.", + "email_password_required": "이메일과 비밀번호는 필수입니다.", + "invalid_role": "역할이 올바르지 않습니다.", + "email_taken": "이미 사용 중인 이메일입니다.", + "username_taken": "이미 사용 중인 아이디입니다.", + "cannot_change_own_status": "자기 자신의 상태를 변경할 수 없습니다.", + "cannot_change_own_role": "자기 자신의 역할을 변경할 수 없습니다.", + "password_required": "비밀번호를 입력해 주세요.", + "cannot_delete_self": "자기 자신을 삭제할 수 없습니다.", + "invalid_role_value": "잘못된 role 값입니다.", + "invalid_status_value": "잘못된 status 값입니다.", + "cannot_change_own_role_status": "자기 자신의 role/status 는 변경할 수 없습니다. 다른 관리자에게 요청해 주세요.", + "last_admin_race": "동시에 다른 관리자도 강등된 것으로 보입니다. 변경을 취소했습니다. 다시 시도해 주세요.", + "select_department": "부서를 선택해 주세요.", + "department_not_found": "부서를 찾을 수 없습니다.", + "position_not_found": "직책을 찾을 수 없습니다.", + "select_team": "팀을 선택해 주세요.", + "team_not_found": "팀을 찾을 수 없습니다.", + "select_part": "파트를 선택해 주세요.", + "part_not_found": "파트를 찾을 수 없습니다.", + "select_service": "서비스를 선택해 주세요.", + "invalid_service_type": "잘못된 서비스 종류입니다.", + "invalid_service_id": "잘못된 서비스 ID 입니다.", + "oidc_client_not_found": "OIDC 클라이언트를 찾을 수 없습니다.", + "saml_sp_not_found": "SAML SP 를 찾을 수 없습니다.", + "role_not_in_service": "선택한 role 이 해당 서비스에 속하지 않습니다.", + "attributes_not_object": "attributesJson 은 JSON object 여야 합니다.", + "attributes_parse_failed": "attributesJson 파싱 실패.", + "assignment_exists": "이미 해당 서비스에 매핑이 존재합니다. 먼저 삭제해 주세요.", + "scope_openid_required": "scope 에는 'openid' 가 포함되어야 합니다.", + "invalid_token_auth_method": "tokenEndpointAuthMethod 값이 올바르지 않습니다.", + "redirect_uri_required": "Redirect URI 는 필수입니다.", + "client_not_found": "클라이언트를 찾을 수 없습니다.", + "invalid_role_key": "key 는 영숫자/._- 만 허용 (1~64자).", + "label_required": "label 은 필수입니다.", + "role_key_exists": "이미 존재하는 role key 입니다.", + "ldap_host_required": "LDAP 호스트는 필수입니다.", + "ldap_bind_or_userdn_required": "Admin Bind DN 또는 유저 DN 패턴 중 하나는 필수입니다.", + "ldap_signing_key_secret_required": "IDP_SIGNING_KEY_SECRET 이 설정되어야 LDAP bindPassword 를 저장할 수 있습니다.", + "invalid_url": "유효한 URL을 입력해 주세요.", + "https_only": "https URL만 허용됩니다.", + "loopback_forbidden": "loopback 주소는 사용할 수 없습니다.", + "internal_addr_forbidden": "내부망/메타데이터 주소는 사용할 수 없습니다.", + "required_fields": "필수 항목을 입력해 주세요.", + "invalid_client_type": "clientType이 올바르지 않습니다.", + "cache_ttl_negative": "cacheTtlSeconds 는 0 이상이어야 합니다.", + "cache_ttl_max": "cacheTtlSeconds 는 {{max}} 초(1일) 를 초과할 수 없습니다.", + "skin_config_exists": "이미 동일한 클라이언트/스킨 타입 설정이 있습니다.", + "skin_not_found": "스킨을 찾을 수 없습니다.", + "url_required": "URL을 입력해 주세요.", + "rotate_conflict": "다른 rotate 작업이 진행 중이거나 DB 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.", + "entity_id_required": "Entity ID 는 필수입니다.", + "acs_url_required": "ACS URL 은 필수입니다.", + "sp_cert_required_for_encryption": "Assertion 암호화를 사용하려면 SP 인증서가 필요합니다.", + "nameid_format_forbidden": "허용되지 않는 NameID Format 입니다.", + "entity_id_taken": "이미 등록된 Entity ID 입니다.", + "login_missing_credentials": "아이디와 비밀번호를 입력해 주세요.", + "login_invalid_credentials": "아이디 또는 비밀번호가 올바르지 않습니다.", + "login_not_admin": "관리자 권한이 없는 계정입니다.", + "login_mfa_config": "MFA 설정 오류: IDP_SIGNING_KEY_SECRET 이 설정되지 않았습니다.", + "login_mfa_required": "관리자 계정은 MFA(OTP) 설정이 필수입니다. 먼저 MFA를 등록해 주세요.", + "login_rate_limit": "로그인 시도가 너무 많습니다. {{minutes}}분 후 다시 시도해 주세요.", + "team_name_required": "팀명을 입력해 주세요.", + "part_name_required": "파트명을 입력해 주세요.", + "position_name_required": "직급명을 입력해 주세요.", + "department_name_required": "부서명을 입력해 주세요.", + "level_must_be_number": "레벨은 숫자여야 합니다.", + "url_invalid_format": "URL 형식이 올바르지 않습니다: {{value}}", + "url_fragment_forbidden": "fragment(#) 가 포함된 URI 는 허용되지 않습니다: {{value}}", + "scheme_forbidden": "{{scheme}}: 스킴은 허용되지 않습니다.", + "http_localhost_only": "http URL 은 localhost/127.0.0.1 만 허용됩니다: {{value}}", + "custom_scheme_invalid": "커스텀 scheme 형식이 올바르지 않습니다: {{value}}", + "scheme_not_allowed": "허용되지 않는 scheme 입니다 ({{scheme}}): {{value}}", + "scope_not_allowed": "허용되지 않는 scope 입니다: {{scope}}" + } }, "admin_login": { "subtitle": "관리자 로그인", @@ -157,7 +241,11 @@ "locale_settings": "지역 설정", "org_membership": "조직 소속", "team_label": "팀", - "part_label": "파트" + "part_label": "파트", + "email_unverified_title": "이메일이 아직 인증되지 않았습니다.", + "email_unverified_desc": "{{email}} 주소로 인증 메일을 보냈습니다. 받은 편지함을 확인해 주세요.", + "email_resend": "인증 메일 재발송", + "email_resend_sent": "인증 메일을 다시 보냈습니다. 잠시 후 받은 편지함을 확인해 주세요." }, "audit": { "title": "감사 로그", @@ -274,6 +362,11 @@ }, "user_detail": { "back": "← 목록", + "danger_zone": "위험 작업", + "force_logout": "강제 로그아웃", + "force_logout_desc": "이 사용자의 모든 세션과 OIDC refresh token 을 즉시 폐기합니다. 상태나 비밀번호는 변경되지 않습니다.", + "force_logout_confirm": "이 사용자를 모든 기기에서 강제 로그아웃하시겠습니까?", + "force_logout_done": "모든 세션이 폐기되었습니다.", "saved": "저장되었습니다.", "profile_section": "프로필 정보", "given_name": "이름 (Given Name)", @@ -467,6 +560,68 @@ "submit": "비밀번호 변경", "invalid_link": "링크가 만료되었거나 유효하지 않습니다." }, + "verify_email": { + "title": "이메일 인증", + "subtitle": "이메일 주소 인증을 완료해 주세요.", + "confirm_prompt": "아래 버튼을 클릭하면 이메일 주소 인증이 완료됩니다.", + "submit": "이메일 인증하기", + "success": "이메일 주소가 인증되었습니다.", + "invalid_link": "링크가 만료되었거나 유효하지 않습니다.", + "go_login": "로그인하러 가기" + }, + "errors": { + "rate_limit": "요청이 너무 많습니다. {{minutes}}분 후 다시 시도해 주세요.", + "db_not_ready": "D1 binding \"DB\" 가 준비되지 않았습니다. Wrangler preview/dev 환경에서 실행해 주세요." + }, + "security_alert": { + "when": "발생 시각: {{time}}", + "footer": "본인이 한 작업이 아니라면 즉시 비밀번호를 변경하고 관리자에게 문의해 주세요.", + "password_changed": { + "subject": "[보안 알림] 비밀번호가 변경되었습니다", + "heading": "비밀번호가 변경되었습니다", + "body": "계정의 비밀번호가 방금 변경되었습니다." + }, + "password_reset_by_admin": { + "subject": "[보안 알림] 관리자가 비밀번호를 초기화했습니다", + "heading": "비밀번호가 초기화되었습니다", + "body": "관리자가 계정의 비밀번호를 초기화했습니다. 새 비밀번호로 로그인해 주세요." + }, + "account_locked": { + "subject": "[보안 알림] 계정이 잠겼습니다", + "heading": "계정이 잠겼습니다", + "body": "계정이 관리자에 의해 잠금 처리되었습니다. 로그인할 수 없습니다." + }, + "account_disabled": { + "subject": "[보안 알림] 계정이 비활성화되었습니다", + "heading": "계정이 비활성화되었습니다", + "body": "계정이 관리자에 의해 비활성화되었습니다. 로그인할 수 없습니다." + }, + "mfa_enrolled": { + "subject": "[보안 알림] 2단계 인증이 등록되었습니다", + "heading": "2단계 인증이 등록되었습니다", + "body": "계정에 2단계 인증(TOTP)이 새로 등록되었습니다." + }, + "mfa_disabled": { + "subject": "[보안 알림] 2단계 인증이 해제되었습니다", + "heading": "2단계 인증이 해제되었습니다", + "body": "계정의 2단계 인증(TOTP)이 삭제되었습니다." + }, + "backup_codes_regenerated": { + "subject": "[보안 알림] 백업 코드가 재생성되었습니다", + "heading": "백업 코드가 재생성되었습니다", + "body": "계정의 2단계 인증 백업 코드가 새로 생성되었습니다. 기존 백업 코드는 더 이상 사용할 수 없습니다." + }, + "passkey_added": { + "subject": "[보안 알림] 새 패스키가 등록되었습니다", + "heading": "새 패스키가 등록되었습니다", + "body": "계정에 새 패스키가 등록되었습니다." + }, + "passkey_removed": { + "subject": "[보안 알림] 패스키가 삭제되었습니다", + "heading": "패스키가 삭제되었습니다", + "body": "계정에서 패스키가 삭제되었습니다." + } + }, "skins": { "title": "로그인 스킨", "add_btn": "+ 스킨 추가", diff --git a/src/lib/server/auth/bootstrap.ts b/src/lib/server/auth/bootstrap.ts index 68811f5..46d9965 100644 --- a/src/lib/server/auth/bootstrap.ts +++ b/src/lib/server/auth/bootstrap.ts @@ -1,8 +1,9 @@ +import { dev } from "$app/environment"; import { and, eq } from "drizzle-orm"; import type { DB } from "$lib/server/db"; import { signingKeys, type Tenant, tenants } from "$lib/server/db/schema"; import { DEFAULT_TENANT_SLUG } from "./constants"; -import { getRuntimeConfig } from "./runtime"; +import { getRuntimeConfig, type RuntimeConfig } from "./runtime"; import { generateRsaSigningKey, generateSelfSignedCert, unwrapPrivateKey, wrapPrivateKey } from "$lib/server/crypto/keys"; function isUniqueConstraintError(error: unknown): boolean { @@ -84,6 +85,34 @@ export async function ensureSigningKey(db: DB, tenant: Tenant, signingKeySecret: }); } +/** + * S5 fail-fast: 프로덕션 필수 환경변수 검증. + * + * `IDP_ISSUER_URL` / `IDP_SIGNING_KEY_SECRET` 는 프로덕션에서 반드시 설정되어야 + * 한다. 미설정이면 토큰 발급 시점(ensureSigningKey 조용한 스킵, resolveIssuerUrl + * Host fallback)이 아니라 요청 초기(ensureAuthBaseline) 에 명확한 오류로 실패시켜 + * 오구성을 즉시 드러낸다. + * + * dev 에서는 로컬 DX(변수 없이 구동) 보존을 위해 검증을 건너뛴다. + * + * 검증 결과는 성공 시 1회만 계산되도록 캐시한다(설정은 isolate 수명 내 불변). + * 실패 시 캐시하지 않으므로 요청마다 재검증되어 fail-closed 를 유지한다. + */ +let requiredConfigValidated = false; +function assertRequiredConfig(config: RuntimeConfig): void { + if (dev || requiredConfigValidated) return; + + const missing: string[] = []; + if (!config.issuerUrl) missing.push("IDP_ISSUER_URL"); + if (!config.signingKeySecret) missing.push("IDP_SIGNING_KEY_SECRET"); + + if (missing.length > 0) { + throw new Error(`프로덕션 필수 환경변수가 설정되지 않았습니다: ${missing.join(", ")}. 배포 환경 변수/시크릿을 확인해 주세요.`); + } + + requiredConfigValidated = true; +} + const BASELINE_TTL_MS = 5 * 60 * 1000; // 5분 interface BaselineCache { @@ -102,6 +131,8 @@ export async function ensureAuthBaseline(db: DB, platform: App.Platform | undefi } const config = getRuntimeConfig(platform); + // 프로덕션 필수값 검증 — DB 작업 전에 요청 초기에 fail-fast. + assertRequiredConfig(config); const tenant = await ensureDefaultTenant(db, platform); if (config.signingKeySecret) { await ensureSigningKey(db, tenant, config.signingKeySecret, config.issuerUrl); diff --git a/src/lib/server/auth/runtime.ts b/src/lib/server/auth/runtime.ts index 61b89a8..848ac48 100644 --- a/src/lib/server/auth/runtime.ts +++ b/src/lib/server/auth/runtime.ts @@ -1,3 +1,6 @@ +import { dev } from "$app/environment"; +import { error } from "@sveltejs/kit"; + export interface RuntimeConfig { defaultTenantName: string; issuerUrl?: string; @@ -34,12 +37,16 @@ export function getRuntimeConfig(platform: App.Platform | undefined): RuntimeCon /** * ctrls C-10 후속 sweep: issuer URL 결정 헬퍼. * - * IDP_ISSUER_URL 환경변수가 설정되어 있으면 그 값을 사용. 미설정 시 들어온 - * 요청의 origin 으로 fallback (dev 환경 호환). production 에서 이 fallback 이 - * 동작하면 Host 헤더 주입으로 iss 클레임이 오염될 수 있어 운영 가시화를 위해 - * console.warn 한 번 남긴다 (cold start 마다 1회). + * IDP_ISSUER_URL 환경변수가 설정되어 있으면 그 값을 사용. + * + * S5 fail-closed: + * - production(`!dev`): 미설정이면 요청 Host 로 fallback 하지 않고 503 오류로 즉시 + * 차단한다. Host 헤더 주입으로 iss 클레임/SAML Issuer 가 오염되는 것을 막는다. + * (부트스트랩 경로에서 요청 초기 검증도 하지만, discovery 등 baseline 을 + * 건너뛰는 라우트를 위한 최종 방어선.) + * - dev: 로컬 DX 보존을 위해 요청 origin fallback + 1회 warn 유지. * - * 본 sweep 의 호스트 주입 영향: + * production 에서 fallback 을 허용했을 때의 호스트 주입 영향: * - discovery/token/userinfo: 발급 토큰의 iss 가 attacker 도메인 — RP 가 * strict iss 검증하면 거부 (대부분의 RP) → self-DoS. RP 가 느슨하면 * attacker 도메인 신뢰로 이어질 수 있음. @@ -47,13 +54,14 @@ export function getRuntimeConfig(platform: App.Platform | undefined): RuntimeCon * verifyIdToken 실패 → self-DoS. * - SAML SLO/logout: SAML 메시지의 Issuer 가 attacker 도메인 — 마찬가지로 * SP 의 strict 검증으로 거부 → self-DoS. - * - * 직접 RCE/탈취는 아니지만 RP 호환성 + 운영 위생 차원에서 정상 발급은 - * issuerUrl 가 명시된 경우에만 발생하도록 한다. */ let warnedMissingIssuer = false; export function resolveIssuerUrl(runtimeConfig: RuntimeConfig | undefined, fallbackOrigin: string): string { if (runtimeConfig?.issuerUrl) return runtimeConfig.issuerUrl; + if (!dev) { + // production fail-closed: Host 주입을 신뢰하지 않는다. + throw error(503, "IDP_ISSUER_URL 이 설정되지 않았습니다. 프로덕션에서는 필수 설정입니다."); + } if (!warnedMissingIssuer) { warnedMissingIssuer = true; console.warn("[runtime] IDP_ISSUER_URL 미설정 — 요청 Host 기반 origin fallback. 운영에서는 반드시 명시할 것."); diff --git a/src/lib/server/auth/users.ts b/src/lib/server/auth/users.ts index 690f243..817a3cd 100644 --- a/src/lib/server/auth/users.ts +++ b/src/lib/server/auth/users.ts @@ -4,6 +4,18 @@ import { credentials, type Credential, type User, users } from "$lib/server/db/s import { PASSWORD_CREDENTIAL_TYPE, TOTP_CREDENTIAL_TYPE } from "./constants"; import { verifyPassword } from "./password"; +// S1(타이밍 계정 열거 차단): username 미존재/비활성/자격증명 부재 경로에서도 실제 scrypt 1회 +// 비용을 발생시켜 존재/미존재 계정의 응답시간을 균등화한다. 아래 상수는 운영 파라미터 +// (scrypt N=2^15, r=8, p=3)와 동일 비용으로 사전 생성한 고정 더미 해시로, 모듈 로드 시 생성하면 +// cold start 비용이 들므로 상수 문자열로 박아 둔다. verifyPassword 는 이 scrypt 레코드에 대해 +// 파생 1회만 수행하고(비교 실패 → rehash 없음) 결과는 폐기된다. +const TIMING_DUMMY_HASH = "scrypt$N=32768,r=8,p=3$laGnY6fbAMkDKdFTKRUGyg==$Jm6an31vv6UDMaa2dn2B2riImIX6qmwMUcc6BWcccg8="; + +async function equalizeAuthTiming(password: string): Promise { + // 결과는 의도적으로 폐기 — 존재/미존재 계정 응답시간 균등화 목적의 더미 검증. + await verifyPassword(password, TIMING_DUMMY_HASH); +} + export function normalizeEmail(email: string): string { return email.trim().toLowerCase(); } @@ -47,12 +59,15 @@ export async function authenticateLocalUser(db: DB, tenantId: string, username: const user = await findUserByUsername(db, tenantId, username); if (!user || user.status !== "active") { + // 존재-오답 경로와 동일하게 scrypt 1회 비용을 태워 타이밍 오라클을 제거. + await equalizeAuthTiming(password); return null; } const credential = await findPasswordCredential(db, user.id); if (!credential?.secret) { + await equalizeAuthTiming(password); return null; } diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts index 4244552..c3d396e 100644 --- a/src/routes/(auth)/login/+page.server.ts +++ b/src/routes/(auth)/login/+page.server.ts @@ -9,7 +9,8 @@ import { AMR_PASSWORD, amrToAcr } from "$lib/server/auth/constants"; import { getRuntimeConfig } from "$lib/server/auth/runtime"; import { checkRateLimit } from "$lib/server/ratelimit"; import { and, eq } from "drizzle-orm"; -import { identityProviders } from "$lib/server/db/schema"; +import type { DB } from "$lib/server/db"; +import { identityProviders, rateLimits } from "$lib/server/db/schema"; import { authenticateLdap } from "$lib/server/ldap/auth"; import { provisionLdapUser } from "$lib/server/ldap/provision"; import type { LdapProviderConfig } from "$lib/server/ldap/types"; @@ -18,6 +19,38 @@ import { resolveSkinHtml, replacePlaceholders, escapeHtml } from "$lib/server/sk import { sanitizeRedirectTarget } from "$lib/server/auth/redirect"; import { translate } from "$lib/i18n/server"; +// S2(계정 단위 잠금): IP 무관하게 동일 계정에 대한 연속 실패를 제한한다. 임계값은 보수적으로 +// 15분 창 10회 실패. 성공 로그인은 카운트하지 않고(오탐 최소화), 실패 분기에서만 checkRateLimit +// 으로 기록한다. 미존재 계정도 제출된 username 키로 동일하게 카운트/응답하므로 사용자 열거 +// 오라클을 만들지 않는다. +const USER_LOCK_WINDOW_MS = 15 * 60 * 1000; +const USER_LOCK_LIMIT = 10; + +// checkRateLimit(두 버킷 슬라이딩 윈도우)의 키/윈도우 규약을 증가 없이 read-only 로 재현한다. +// checkRateLimit 은 호출 시 카운터를 선증가시켜 "성공 미카운트 + 실패 시에만 기록" 요건과 +// 충돌하므로, 상단 잠금 판정에는 증가 없는 조회를 쓰고 실제 기록은 실패 분기에서만 수행한다. +async function accountLockStatus(db: DB, key: string, windowMs: number, limit: number): Promise<{ locked: boolean; retryAfterMs: number }> { + const now = Date.now(); + const windowIndex = Math.floor(now / windowMs); + const windowStart = windowIndex * windowMs; + const elapsed = now - windowStart; + + const [cur] = await db + .select({ count: rateLimits.count }) + .from(rateLimits) + .where(eq(rateLimits.key, `${key}:${windowIndex}`)) + .limit(1); + const [prev] = await db + .select({ count: rateLimits.count }) + .from(rateLimits) + .where(eq(rateLimits.key, `${key}:${windowIndex - 1}`)) + .limit(1); + + const slidingCount = Math.floor((prev?.count ?? 0) * (1 - elapsed / windowMs)) + (cur?.count ?? 0); + const locked = slidingCount > limit; + return { locked, retryAfterMs: locked ? windowMs - elapsed : 0 }; +} + async function resolveSkinForAction(event: Parameters[0], flashMsg: string, redirectTo: string | null): Promise { const skinHint = event.url.searchParams.get("skinHint"); if (!skinHint || !event.locals.db || !event.locals.tenant) return null; @@ -129,6 +162,29 @@ export const actions: Actions = { }); } + // 계정 단위 잠금(S2): 인증 시도 전에 잠금 여부를 증가 없이 조회해 조기 차단한다. + // (LDAP/로컬 인증 이전이라 두 경로 모두 보호되고, 잠긴 계정은 올바른 비밀번호로도 + // 진입할 수 없어 scrypt 비용 낭비와 열거 오라클을 함께 차단한다.) + const userLockKey = `login:user:${username}`; + const lock = await accountLockStatus(db, userLockKey, USER_LOCK_WINDOW_MS, USER_LOCK_LIMIT); + if (lock.locked) { + await recordAuditEvent(db, { + tenantId: tenant.id, + kind: "login", + outcome: "failure", + ip: requestMetadata.ip, + userAgent: requestMetadata.userAgent, + detail: { username, reason: "account_locked" }, + }); + const msg = translate(locale, "login.err_account_locked", { minutes: Math.ceil(lock.retryAfterMs / 60000) }); + return fail(429, { + username, + redirectTo, + error: msg, + skinHtml: await resolveSkinForAction(event, msg, redirectTo), + }); + } + // LDAP 프로바이더가 설정된 경우 먼저 시도 const [ldapProvider] = await db .select() @@ -185,6 +241,11 @@ export const actions: Actions = { } if (!user) { + // 실패 시에만 카운트(성공은 미카운트). 미존재/존재-오답 모두 이 분기를 타므로 + // 동일하게 기록되어 열거 오라클을 만들지 않는다. 임계 초과는 다음 요청의 + // accountLockStatus 조기 차단에서 반영된다. + await checkRateLimit(db, userLockKey, { windowMs: USER_LOCK_WINDOW_MS, limit: USER_LOCK_LIMIT }); + await recordAuditEvent(db, { tenantId: tenant.id, kind: "login", diff --git a/src/routes/api/users/lookup/+server.ts b/src/routes/api/users/lookup/+server.ts index 5b9b834..4397b49 100644 --- a/src/routes/api/users/lookup/+server.ts +++ b/src/routes/api/users/lookup/+server.ts @@ -11,8 +11,12 @@ import { DEFAULT_TENANT_SLUG } from "$lib/server/auth/constants"; * 신뢰된 다른 서비스 (예: stardust dispatcher) 가 username/email → users.id (uuid) * 매핑이 필요할 때 호출. Bearer service-token 으로 보호. * - * Query 파라미터 (셋 중 하나): - * - `?id=` : 직접 user id (tenant 무관) + * 모든 조회는 tenant 스코프로 강제된다. `tenant` 슬러그 미지정 시 default 테넌트로 + * 폴백하며(username/email 경로와 동일), id 경로도 예외 없이 해당 tenant 소속만 조회한다. + * 전역 dispatcher service-token 이 임의 테넌트 사용자 레코드를 조회하는 것을 막는다. + * + * Query 파라미터 (셋 중 하나 + 선택적 tenant): + * - `?id=&tenant=` : user id (해당 tenant 소속일 때만) * - `?username=&tenant=` : tenant 슬러그 생략 시 default * - `?email=&tenant=` : email 매칭 (lowercase 비교) * @@ -32,15 +36,19 @@ export const GET: RequestHandler = async ({ request, url, locals }) => { throw error(400, "one of id / username / email required"); } + const [tenant] = await db.select({ id: tenants.id }).from(tenants).where(eq(tenants.slug, tenantSlug)).limit(1); + if (!tenant) throw error(404, `tenant not found: ${tenantSlug}`); + if (id) { - const [row] = await db.select().from(users).where(eq(users.id, id)).limit(1); + const [row] = await db + .select() + .from(users) + .where(and(eq(users.tenantId, tenant.id), eq(users.id, id))) + .limit(1); if (!row) throw error(404, "user not found"); return json(shape(row)); } - const [tenant] = await db.select({ id: tenants.id }).from(tenants).where(eq(tenants.slug, tenantSlug)).limit(1); - if (!tenant) throw error(404, `tenant not found: ${tenantSlug}`); - if (username) { const [row] = await db .select() diff --git a/src/routes/oidc/token/+server.ts b/src/routes/oidc/token/+server.ts index 192174d..a3bc4c5 100644 --- a/src/routes/oidc/token/+server.ts +++ b/src/routes/oidc/token/+server.ts @@ -42,6 +42,28 @@ function clientAllowsGrant(client: OidcClientRecord, grantType: string): boolean .includes(grantType); } +interface GrantSessionCheck { + row: typeof sessions.$inferSelect | null; + revoked: boolean; + expired: boolean; +} + +/** + * grant/refresh 에 연결된 IdP 세션을 조회하고 폐기(revoked)·만료(expired) 여부를 판정한다. + * authorization_code / refresh_token 두 grant 경로에서 공통으로 사용한다. + * 세션 row 가 없으면(예: onDelete set null 이전에 삭제) revoked/expired 모두 false 로 두어 + * 호출부의 기존 관례(row 부재 시 거부하지 않음)를 보존한다. + */ +async function checkGrantSession(db: DB, sessionId: string): Promise { + const [row] = await db.select().from(sessions).where(eq(sessions.id, sessionId)).limit(1); + const session = row ?? null; + return { + row: session, + revoked: Boolean(session?.revokedAt), + expired: Boolean(session && session.expiresAt.getTime() <= Date.now()), + }; +} + interface BuildTokenParams { db: DB; tenantId: string; @@ -307,9 +329,10 @@ export const POST: RequestHandler = async (event) => { // 브라우저 세션보다 오래 살아야 하기 때문. (로그아웃 시 refresh token 은 별도 폐기됨.) let sessionRow: typeof sessions.$inferSelect | null = null; if (record.sessionId) { - const [s] = await db.select().from(sessions).where(eq(sessions.id, record.sessionId)).limit(1); - sessionRow = s ?? null; - if (sessionRow?.revokedAt) { + const check = await checkGrantSession(db, record.sessionId); + sessionRow = check.row; + // 명시적 폐기(revoked)만 거부한다 — 세션의 자연 만료(expired)는 위 주석대로 거부하지 않는다. + if (check.revoked) { // 방금 회전된 토큰을 포함해 family 를 폐기하고 거부. await revokeRefreshTokenFamily(db, tenant.id, record.userId, clientId); await recordTokenFailure(clientId, "invalid_grant", "연결된 세션이 로그아웃됨"); @@ -413,6 +436,17 @@ export const POST: RequestHandler = async (event) => { } } + // 연결된 IdP 세션이 로그아웃(폐기)됐거나 만료됐으면 거부한다. + // 로그아웃 후에도 5분 TTL 안의 미소진 code 로 토큰이 발급되는 것을 막는다. + // sessionId 가 null 인 grant(세션 삭제로 set null 된 경우 등)는 검사를 건너뛰어 기존 동작을 보존한다. + if (grant.sessionId) { + const sessionCheck = await checkGrantSession(db, grant.sessionId); + if (sessionCheck.revoked || sessionCheck.expired) { + await recordTokenFailure(clientId, "invalid_grant", sessionCheck.revoked ? "연결된 세션이 로그아웃됨" : "연결된 세션이 만료됨"); + return tokenError("invalid_grant", "로그아웃되었거나 만료된 세션입니다. 다시 로그인해 주세요."); + } + } + const user = await findActiveUserById(db, grant.userId); if (!user) { await recordTokenFailure(clientId, "invalid_grant", "사용자 조회 실패"); From 5d71a4da9af1a10abef684d66a2494462ff27595 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 11:13:06 +0900 Subject: [PATCH 02/26] =?UTF-8?q?chore(db):=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=204=ED=8A=B8=EB=9E=99=20=EB=8C=80?= =?UTF-8?q?=EC=B9=AD=ED=99=94=20=E2=80=94=20legacy=20code=20drop=C2=B7clie?= =?UTF-8?q?nt=5Fskins=20=EB=B3=B4=EC=A0=95=C2=B7email=5Fverification=5Ftok?= =?UTF-8?q?ens=C2=B7CI=20=EB=93=9C=EB=A6=AC=ED=94=84=ED=8A=B8=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - legacy oidc_grants.code 컬럼+uidx 3방언 drop (codeHash 전환 완료분 정리) - client_skins.createdAt sqlite 초→ms 통일 + 기존 값 ×1000 보정 UPDATE(재적용 안전 가드) - email_verification_tokens 테이블 3방언 추가 (password_reset_tokens 패턴) - d1 offline generate 침묵 버그 수정: d1-http driver 우회(CLI 플래그), 적용(db:migrate)은 config 유지 - CI에 마이그레이션 드리프트 체크 스텝(db:generate:all 후 git diff) - 4트랙 생성물: d1 0021·0022 / pg·mysql·sqlite 각 0004·0005 — 적용은 별도(사용자) Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 8 + drizzle.config.ts | 3 +- drizzle/0021_new_darkhawk.sql | 6 + drizzle/0022_chemical_thena.sql | 12 + drizzle/meta/0021_snapshot.json | 3779 ++++++++++++++++++++ drizzle/meta/0022_snapshot.json | 3861 ++++++++++++++++++++ drizzle/meta/_journal.json | 14 + drizzle/mysql/0004_bizarre_jazinda.sql | 2 + drizzle/mysql/0005_tan_green_goblin.sql | 13 + drizzle/mysql/meta/0004_snapshot.json | 3960 +++++++++++++++++++++ drizzle/mysql/meta/0005_snapshot.json | 4049 +++++++++++++++++++++ drizzle/mysql/meta/_journal.json | 14 + drizzle/pg/0004_yummy_midnight.sql | 2 + drizzle/pg/0005_damp_bruce_banner.sql | 12 + drizzle/pg/meta/0004_snapshot.json | 4235 ++++++++++++++++++++++ drizzle/pg/meta/0005_snapshot.json | 4330 +++++++++++++++++++++++ drizzle/pg/meta/_journal.json | 14 + drizzle/sqlite/0004_next_scarecrow.sql | 6 + drizzle/sqlite/0005_early_black_cat.sql | 12 + drizzle/sqlite/meta/0004_snapshot.json | 3779 ++++++++++++++++++++ drizzle/sqlite/meta/0005_snapshot.json | 3861 ++++++++++++++++++++ drizzle/sqlite/meta/_journal.json | 14 + src/lib/server/db/schema.mysql.ts | 34 +- src/lib/server/db/schema.pg.ts | 32 +- src/lib/server/db/schema.sqlite.ts | 38 +- src/lib/server/oidc/grant.ts | 5 +- 26 files changed, 32069 insertions(+), 26 deletions(-) create mode 100644 drizzle/0021_new_darkhawk.sql create mode 100644 drizzle/0022_chemical_thena.sql create mode 100644 drizzle/meta/0021_snapshot.json create mode 100644 drizzle/meta/0022_snapshot.json create mode 100644 drizzle/mysql/0004_bizarre_jazinda.sql create mode 100644 drizzle/mysql/0005_tan_green_goblin.sql create mode 100644 drizzle/mysql/meta/0004_snapshot.json create mode 100644 drizzle/mysql/meta/0005_snapshot.json create mode 100644 drizzle/pg/0004_yummy_midnight.sql create mode 100644 drizzle/pg/0005_damp_bruce_banner.sql create mode 100644 drizzle/pg/meta/0004_snapshot.json create mode 100644 drizzle/pg/meta/0005_snapshot.json create mode 100644 drizzle/sqlite/0004_next_scarecrow.sql create mode 100644 drizzle/sqlite/0005_early_black_cat.sql create mode 100644 drizzle/sqlite/meta/0004_snapshot.json create mode 100644 drizzle/sqlite/meta/0005_snapshot.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87ebf77..9ce1403 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,6 +82,14 @@ jobs: - name: Test run: bun run test + - name: Migration drift check + run: | + bun run db:generate:all + if ! git diff --exit-code -- drizzle; then + echo "::error::스키마가 변경되었으나 마이그레이션이 생성되지 않았습니다. 'bun run db:generate:all' 실행 후 drizzle*/ 변경분을 커밋하세요." >&2 + exit 1 + fi + - name: Build run: bun run build diff --git a/drizzle.config.ts b/drizzle.config.ts index d313189..c477bee 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -6,7 +6,8 @@ import { defineConfig } from "drizzle-kit"; const DB_DIALECT: string = process.env.DB_DIALECT || "d1"; // 방언별 스키마 파일과 마이그레이션 출력 디렉터리를 분리한다. -// (drizzle/ 는 .gitignore 대상 — 로컬 생성물) +// (drizzle*/ 마이그레이션 트랙은 커밋 대상 — 4방언 스냅샷/journal 을 형상 관리한다. +// db:generate 는 로컬 스냅샷 기준으로 diff 하며 자격증명 없이 동작한다.) function pgConfig() { return defineConfig({ schema: "./src/lib/server/db/schema.pg.ts", diff --git a/drizzle/0021_new_darkhawk.sql b/drizzle/0021_new_darkhawk.sql new file mode 100644 index 0000000..991675f --- /dev/null +++ b/drizzle/0021_new_darkhawk.sql @@ -0,0 +1,6 @@ +DROP INDEX `oidc_grants_code_uidx`;--> statement-breakpoint +ALTER TABLE `oidc_grants` DROP COLUMN `code`;--> statement-breakpoint +-- [manual] client_skins.created_at 초 단위(legacy timestamp) → ms 단위(timestamp_ms) 보정. +-- 스키마상 컬럼 타입은 integer 로 동일해 drizzle-kit 가 DDL diff 를 만들지 못하므로 수동 추가. +-- ms 값은 ~1.7e12, 초 값은 ~1.7e9 이므로 1e11 미만 행만 초 단위로 판별해 ×1000 (재적용 안전). +UPDATE `client_skins` SET `created_at` = `created_at` * 1000 WHERE `created_at` < 100000000000; \ No newline at end of file diff --git a/drizzle/0022_chemical_thena.sql b/drizzle/0022_chemical_thena.sql new file mode 100644 index 0000000..d33ec08 --- /dev/null +++ b/drizzle/0022_chemical_thena.sql @@ -0,0 +1,12 @@ +CREATE TABLE `email_verification_tokens` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `token_hash` text NOT NULL, + `expires_at` integer NOT NULL, + `used_at` integer, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `email_verification_tokens_user_idx` ON `email_verification_tokens` (`user_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `email_verification_tokens_hash_uidx` ON `email_verification_tokens` (`token_hash`); \ No newline at end of file diff --git a/drizzle/meta/0021_snapshot.json b/drizzle/meta/0021_snapshot.json new file mode 100644 index 0000000..c987a5b --- /dev/null +++ b/drizzle/meta/0021_snapshot.json @@ -0,0 +1,3779 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "caab5a82-320b-450c-83fb-078e9579ce4a", + "prevId": "cd086427-88fb-49fb-bab5-18d7806613e0", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_login_at": { + "name": "last_login_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "rotated_at": { + "name": "rotated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + "tenant_id" + ], + "isUnique": true, + "where": "active = 1" + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0022_snapshot.json b/drizzle/meta/0022_snapshot.json new file mode 100644 index 0000000..9ad4aa5 --- /dev/null +++ b/drizzle/meta/0022_snapshot.json @@ -0,0 +1,3861 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "7cb45096-8efb-402b-bc70-b9b25f7d37c9", + "prevId": "caab5a82-320b-450c-83fb-078e9579ce4a", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_verification_tokens": { + "name": "email_verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_login_at": { + "name": "last_login_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "rotated_at": { + "name": "rotated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + "tenant_id" + ], + "isUnique": true, + "where": "active = 1" + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 9ab53be..197fb47 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -148,6 +148,20 @@ "when": 1783246066470, "tag": "0020_cool_firebrand", "breakpoints": true + }, + { + "idx": 21, + "version": "6", + "when": 1783297607757, + "tag": "0021_new_darkhawk", + "breakpoints": true + }, + { + "idx": 22, + "version": "6", + "when": 1783299187869, + "tag": "0022_chemical_thena", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/mysql/0004_bizarre_jazinda.sql b/drizzle/mysql/0004_bizarre_jazinda.sql new file mode 100644 index 0000000..1d38793 --- /dev/null +++ b/drizzle/mysql/0004_bizarre_jazinda.sql @@ -0,0 +1,2 @@ +ALTER TABLE `oidc_grants` DROP INDEX `oidc_grants_code_uidx`;--> statement-breakpoint +ALTER TABLE `oidc_grants` DROP COLUMN `code`; \ No newline at end of file diff --git a/drizzle/mysql/0005_tan_green_goblin.sql b/drizzle/mysql/0005_tan_green_goblin.sql new file mode 100644 index 0000000..45463b7 --- /dev/null +++ b/drizzle/mysql/0005_tan_green_goblin.sql @@ -0,0 +1,13 @@ +CREATE TABLE `email_verification_tokens` ( + `id` varchar(64) NOT NULL, + `user_id` varchar(64) NOT NULL, + `token_hash` varchar(255) NOT NULL, + `expires_at` datetime(3) NOT NULL, + `used_at` datetime(3), + `created_at` datetime(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP(3)), + CONSTRAINT `email_verification_tokens_id` PRIMARY KEY(`id`), + CONSTRAINT `email_verification_tokens_hash_uidx` UNIQUE(`token_hash`) +); +--> statement-breakpoint +ALTER TABLE `email_verification_tokens` ADD CONSTRAINT `email_verification_tokens_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX `email_verification_tokens_user_idx` ON `email_verification_tokens` (`user_id`); \ No newline at end of file diff --git a/drizzle/mysql/meta/0004_snapshot.json b/drizzle/mysql/meta/0004_snapshot.json new file mode 100644 index 0000000..8f3ab5e --- /dev/null +++ b/drizzle/mysql/meta/0004_snapshot.json @@ -0,0 +1,3960 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "92612a56-2756-4138-ba9c-33b3529f34de", + "prevId": "373a9ae6-201b-430a-8654-ee3e337f697c", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_events_id": { + "name": "audit_events_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "client_skins_id": { + "name": "client_skins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credentials_id": { + "name": "credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "departments_id": { + "name": "departments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "last_login_at": { + "name": "last_login_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "identities_id": { + "name": "identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "identity_providers_id": { + "name": "identity_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('openid')" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('authorization_code,refresh_token')" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('code')" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('RS256')" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_clients_id": { + "name": "oidc_clients_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_grants_id": { + "name": "oidc_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_refresh_tokens_id": { + "name": "oidc_refresh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "parts_id": { + "name": "parts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "password_reset_tokens_id": { + "name": "password_reset_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "positions_id": { + "name": "positions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rate_limits_key": { + "name": "rate_limits_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_sessions_id": { + "name": "saml_sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_slo_states_id": { + "name": "saml_slo_states_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_sps_id": { + "name": "saml_sps_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_roles_id": { + "name": "service_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "rotated_at": { + "name": "rotated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "signing_keys_id": { + "name": "signing_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "teams_id": { + "name": "teams_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tenants_id": { + "name": "tenants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_departments_id": { + "name": "user_departments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_parts_id": { + "name": "user_parts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_service_assignments_id": { + "name": "user_service_assignments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_teams_id": { + "name": "user_teams_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('ko-KR')" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('Asia/Seoul')" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_challenges_id": { + "name": "webauthn_challenges_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0005_snapshot.json b/drizzle/mysql/meta/0005_snapshot.json new file mode 100644 index 0000000..7c29812 --- /dev/null +++ b/drizzle/mysql/meta/0005_snapshot.json @@ -0,0 +1,4049 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "d4867126-5d51-4da6-8502-2e0743da265b", + "prevId": "92612a56-2756-4138-ba9c-33b3529f34de", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_events_id": { + "name": "audit_events_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "client_skins_id": { + "name": "client_skins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credentials_id": { + "name": "credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "departments_id": { + "name": "departments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "email_verification_tokens": { + "name": "email_verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "email_verification_tokens_id": { + "name": "email_verification_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "last_login_at": { + "name": "last_login_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "identities_id": { + "name": "identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "identity_providers_id": { + "name": "identity_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('openid')" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('authorization_code,refresh_token')" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('code')" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('RS256')" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_clients_id": { + "name": "oidc_clients_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_grants_id": { + "name": "oidc_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_refresh_tokens_id": { + "name": "oidc_refresh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "parts_id": { + "name": "parts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "password_reset_tokens_id": { + "name": "password_reset_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "positions_id": { + "name": "positions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rate_limits_key": { + "name": "rate_limits_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_sessions_id": { + "name": "saml_sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_slo_states_id": { + "name": "saml_slo_states_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_sps_id": { + "name": "saml_sps_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_roles_id": { + "name": "service_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "rotated_at": { + "name": "rotated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "signing_keys_id": { + "name": "signing_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "teams_id": { + "name": "teams_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tenants_id": { + "name": "tenants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_departments_id": { + "name": "user_departments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_parts_id": { + "name": "user_parts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_service_assignments_id": { + "name": "user_service_assignments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_teams_id": { + "name": "user_teams_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('ko-KR')" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('Asia/Seoul')" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_challenges_id": { + "name": "webauthn_challenges_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/_journal.json b/drizzle/mysql/meta/_journal.json index 2d05c0e..db13671 100644 --- a/drizzle/mysql/meta/_journal.json +++ b/drizzle/mysql/meta/_journal.json @@ -29,6 +29,20 @@ "when": 1783246072878, "tag": "0003_shiny_abomination", "breakpoints": true + }, + { + "idx": 4, + "version": "5", + "when": 1783297484807, + "tag": "0004_bizarre_jazinda", + "breakpoints": true + }, + { + "idx": 5, + "version": "5", + "when": 1783299188515, + "tag": "0005_tan_green_goblin", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/pg/0004_yummy_midnight.sql b/drizzle/pg/0004_yummy_midnight.sql new file mode 100644 index 0000000..4ab7909 --- /dev/null +++ b/drizzle/pg/0004_yummy_midnight.sql @@ -0,0 +1,2 @@ +DROP INDEX "oidc_grants_code_uidx";--> statement-breakpoint +ALTER TABLE "oidc_grants" DROP COLUMN "code"; \ No newline at end of file diff --git a/drizzle/pg/0005_damp_bruce_banner.sql b/drizzle/pg/0005_damp_bruce_banner.sql new file mode 100644 index 0000000..4e4d81d --- /dev/null +++ b/drizzle/pg/0005_damp_bruce_banner.sql @@ -0,0 +1,12 @@ +CREATE TABLE "email_verification_tokens" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "token_hash" text NOT NULL, + "expires_at" timestamp (3) with time zone NOT NULL, + "used_at" timestamp (3) with time zone, + "created_at" timestamp (3) with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "email_verification_tokens" ADD CONSTRAINT "email_verification_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "email_verification_tokens_user_idx" ON "email_verification_tokens" USING btree ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "email_verification_tokens_hash_uidx" ON "email_verification_tokens" USING btree ("token_hash"); \ No newline at end of file diff --git a/drizzle/pg/meta/0004_snapshot.json b/drizzle/pg/meta/0004_snapshot.json new file mode 100644 index 0000000..cc7fa30 --- /dev/null +++ b/drizzle/pg/meta/0004_snapshot.json @@ -0,0 +1,4235 @@ +{ + "id": "836b4ce0-2406-4e88-b1eb-ffa3e2e68b31", + "prevId": "986db2b0-8890-4c93-ac48-2d7659dda362", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_events": { + "name": "audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_skins": { + "name": "client_skins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skin_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + { + "expression": "totp_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.departments": { + "name": "departments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.identities": { + "name": "identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.identity_providers": { + "name": "identity_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_clients": { + "name": "oidc_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_grants": { + "name": "oidc_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.parts": { + "name": "parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_tokens": { + "name": "password_reset_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.positions": { + "name": "positions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limits": { + "name": "rate_limits", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "schema": "", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seen_at": { + "name": "seen_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_sessions": { + "name": "saml_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + { + "expression": "session_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sp_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_slo_states": { + "name": "saml_slo_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_sps": { + "name": "saml_sps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.service_roles": { + "name": "service_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + { + "expression": "idp_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signing_keys": { + "name": "signing_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "not_after": { + "name": "not_after", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"signing_keys\".\"active\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + { + "expression": "department_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenants": { + "name": "tenants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_departments": { + "name": "user_departments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + { + "expression": "department_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_parts": { + "name": "user_parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + { + "expression": "part_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_service_assignments": { + "name": "user_service_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_teams": { + "name": "user_teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_challenges": { + "name": "webauthn_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "challenge", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/pg/meta/0005_snapshot.json b/drizzle/pg/meta/0005_snapshot.json new file mode 100644 index 0000000..2e64c8e --- /dev/null +++ b/drizzle/pg/meta/0005_snapshot.json @@ -0,0 +1,4330 @@ +{ + "id": "a6fc6d44-6265-40cf-9600-e1fe6331e228", + "prevId": "836b4ce0-2406-4e88-b1eb-ffa3e2e68b31", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_events": { + "name": "audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_skins": { + "name": "client_skins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skin_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + { + "expression": "totp_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.departments": { + "name": "departments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_verification_tokens": { + "name": "email_verification_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.identities": { + "name": "identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.identity_providers": { + "name": "identity_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_clients": { + "name": "oidc_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_grants": { + "name": "oidc_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.parts": { + "name": "parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_tokens": { + "name": "password_reset_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.positions": { + "name": "positions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limits": { + "name": "rate_limits", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "schema": "", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seen_at": { + "name": "seen_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_sessions": { + "name": "saml_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + { + "expression": "session_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sp_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_slo_states": { + "name": "saml_slo_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_sps": { + "name": "saml_sps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.service_roles": { + "name": "service_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + { + "expression": "idp_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signing_keys": { + "name": "signing_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "not_after": { + "name": "not_after", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"signing_keys\".\"active\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + { + "expression": "department_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenants": { + "name": "tenants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_departments": { + "name": "user_departments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + { + "expression": "department_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_parts": { + "name": "user_parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + { + "expression": "part_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_service_assignments": { + "name": "user_service_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_teams": { + "name": "user_teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_challenges": { + "name": "webauthn_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "challenge", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/pg/meta/_journal.json b/drizzle/pg/meta/_journal.json index 43e54d7..51cbee8 100644 --- a/drizzle/pg/meta/_journal.json +++ b/drizzle/pg/meta/_journal.json @@ -29,6 +29,20 @@ "when": 1783246072531, "tag": "0003_charming_the_fury", "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1783297484222, + "tag": "0004_yummy_midnight", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1783299188210, + "tag": "0005_damp_bruce_banner", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/sqlite/0004_next_scarecrow.sql b/drizzle/sqlite/0004_next_scarecrow.sql new file mode 100644 index 0000000..991675f --- /dev/null +++ b/drizzle/sqlite/0004_next_scarecrow.sql @@ -0,0 +1,6 @@ +DROP INDEX `oidc_grants_code_uidx`;--> statement-breakpoint +ALTER TABLE `oidc_grants` DROP COLUMN `code`;--> statement-breakpoint +-- [manual] client_skins.created_at 초 단위(legacy timestamp) → ms 단위(timestamp_ms) 보정. +-- 스키마상 컬럼 타입은 integer 로 동일해 drizzle-kit 가 DDL diff 를 만들지 못하므로 수동 추가. +-- ms 값은 ~1.7e12, 초 값은 ~1.7e9 이므로 1e11 미만 행만 초 단위로 판별해 ×1000 (재적용 안전). +UPDATE `client_skins` SET `created_at` = `created_at` * 1000 WHERE `created_at` < 100000000000; \ No newline at end of file diff --git a/drizzle/sqlite/0005_early_black_cat.sql b/drizzle/sqlite/0005_early_black_cat.sql new file mode 100644 index 0000000..d33ec08 --- /dev/null +++ b/drizzle/sqlite/0005_early_black_cat.sql @@ -0,0 +1,12 @@ +CREATE TABLE `email_verification_tokens` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `token_hash` text NOT NULL, + `expires_at` integer NOT NULL, + `used_at` integer, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `email_verification_tokens_user_idx` ON `email_verification_tokens` (`user_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `email_verification_tokens_hash_uidx` ON `email_verification_tokens` (`token_hash`); \ No newline at end of file diff --git a/drizzle/sqlite/meta/0004_snapshot.json b/drizzle/sqlite/meta/0004_snapshot.json new file mode 100644 index 0000000..423378e --- /dev/null +++ b/drizzle/sqlite/meta/0004_snapshot.json @@ -0,0 +1,3779 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "f0284c74-60fa-4a18-9830-1b2ddbaf073a", + "prevId": "fac94d5e-7925-4f87-a008-a847aab196d2", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_login_at": { + "name": "last_login_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "rotated_at": { + "name": "rotated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + "tenant_id" + ], + "isUnique": true, + "where": "active = 1" + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0005_snapshot.json b/drizzle/sqlite/meta/0005_snapshot.json new file mode 100644 index 0000000..eb36ab7 --- /dev/null +++ b/drizzle/sqlite/meta/0005_snapshot.json @@ -0,0 +1,3861 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "2bd259bd-8f51-4732-8062-2da656626078", + "prevId": "f0284c74-60fa-4a18-9830-1b2ddbaf073a", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_verification_tokens": { + "name": "email_verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_login_at": { + "name": "last_login_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "rotated_at": { + "name": "rotated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + "tenant_id" + ], + "isUnique": true, + "where": "active = 1" + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/_journal.json b/drizzle/sqlite/meta/_journal.json index 1ede302..4f25841 100644 --- a/drizzle/sqlite/meta/_journal.json +++ b/drizzle/sqlite/meta/_journal.json @@ -29,6 +29,20 @@ "when": 1783246073168, "tag": "0003_white_speed_demon", "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1783297485112, + "tag": "0004_next_scarecrow", + "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1783299188782, + "tag": "0005_early_black_cat", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/server/db/schema.mysql.ts b/src/lib/server/db/schema.mysql.ts index 5d0c507..b0eda47 100644 --- a/src/lib/server/db/schema.mysql.ts +++ b/src/lib/server/db/schema.mysql.ts @@ -273,10 +273,8 @@ export const oidcGrants = mysqlTable( .notNull() .references(() => users.id, { onDelete: "cascade" }), sessionId: varchar("session_id", { length: 64 }).references(() => sessions.id, { onDelete: "set null" }), - // ctrls C-6: authorization code 평문 저장 제거. - // 신규 grant 는 codeHash (SHA-256) 만 저장. code 평문 컬럼은 legacy 호환 - // 위해 nullable 로 유지 — 다음 PR 에서 컬럼 자체 drop. - code: varchar("code", { length: 255 }), + // ctrls C-6: authorization code 평문 저장 제거. 신규/기존 grant 모두 codeHash + // (SHA-256) 만 저장한다. (legacy code 평문 컬럼은 본 PR 에서 drop 완료.) codeHash: varchar("code_hash", { length: 255 }), codeChallenge: text("code_challenge"), codeChallengeMethod: varchar("code_challenge_method", { length: 64, enum: ["S256", "plain"] }), @@ -292,9 +290,7 @@ export const oidcGrants = mysqlTable( .default(sql`(CURRENT_TIMESTAMP(3))`), }, (t) => [ - // 기존 code 평문 unique 는 유지 (legacy grants 가 남아 있는 동안). NULL 다중 허용. - uniqueIndex("oidc_grants_code_uidx").on(t.code), - // codeHash unique — 신규 grant 의 1회용 invariant. NULL 다중 허용 (legacy row). + // codeHash unique — grant 의 1회용 invariant. NULL 다중 허용 (legacy row). uniqueIndex("oidc_grants_code_hash_uidx").on(t.codeHash), index("oidc_grants_tenant_client_idx").on(t.tenantId, t.clientId), index("oidc_grants_expires_idx").on(t.expiresAt), @@ -892,6 +888,30 @@ export const passwordResetTokens = mysqlTable( export type PasswordResetToken = typeof passwordResetTokens.$inferSelect; +// ---------- Email Verification ---------- +// password_reset_tokens 와 동일 패턴(SHA-256 해시 저장, TTL, 1회용). TTL 24시간. + +export const emailVerificationTokens = mysqlTable( + "email_verification_tokens", + { + id: varchar("id", { length: 64 }) + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + userId: varchar("user_id", { length: 64 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + tokenHash: varchar("token_hash", { length: 255 }).notNull(), + expiresAt: datetime("expires_at", { mode: "date", fsp: 3 }).notNull(), + usedAt: datetime("used_at", { mode: "date", fsp: 3 }), + createdAt: datetime("created_at", { mode: "date", fsp: 3 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP(3))`), + }, + (t) => [index("email_verification_tokens_user_idx").on(t.userId), uniqueIndex("email_verification_tokens_hash_uidx").on(t.tokenHash)], +); + +export type EmailVerificationToken = typeof emailVerificationTokens.$inferSelect; + export type User = typeof users.$inferSelect; export type Credential = typeof credentials.$inferSelect; export type Identity = typeof identities.$inferSelect; diff --git a/src/lib/server/db/schema.pg.ts b/src/lib/server/db/schema.pg.ts index 0bdd96c..1f5952a 100644 --- a/src/lib/server/db/schema.pg.ts +++ b/src/lib/server/db/schema.pg.ts @@ -245,10 +245,8 @@ export const oidcGrants = pgTable( .notNull() .references(() => users.id, { onDelete: "cascade" }), sessionId: text("session_id").references(() => sessions.id, { onDelete: "set null" }), - // ctrls C-6: authorization code 평문 저장 제거. - // 신규 grant 는 codeHash (SHA-256) 만 저장. code 평문 컬럼은 legacy 호환 - // 위해 nullable 로 유지 — 다음 PR 에서 컬럼 자체 drop. - code: text("code"), + // ctrls C-6: authorization code 평문 저장 제거. 신규/기존 grant 모두 codeHash + // (SHA-256) 만 저장한다. (legacy code 평문 컬럼은 본 PR 에서 drop 완료.) codeHash: text("code_hash"), codeChallenge: text("code_challenge"), codeChallengeMethod: text("code_challenge_method", { enum: ["S256", "plain"] }), @@ -262,9 +260,7 @@ export const oidcGrants = pgTable( createdAt: timestamp("created_at", { mode: "date", withTimezone: true, precision: 3 }).notNull().defaultNow(), }, (t) => [ - // 기존 code 평문 unique 는 유지 (legacy grants 가 남아 있는 동안). NULL 다중 허용. - uniqueIndex("oidc_grants_code_uidx").on(t.code), - // codeHash unique — 신규 grant 의 1회용 invariant. NULL 다중 허용 (legacy row). + // codeHash unique — grant 의 1회용 invariant. NULL 다중 허용 (legacy row). uniqueIndex("oidc_grants_code_hash_uidx").on(t.codeHash), index("oidc_grants_tenant_client_idx").on(t.tenantId, t.clientId), index("oidc_grants_expires_idx").on(t.expiresAt), @@ -810,6 +806,28 @@ export const passwordResetTokens = pgTable( export type PasswordResetToken = typeof passwordResetTokens.$inferSelect; +// ---------- Email Verification ---------- +// password_reset_tokens 와 동일 패턴(SHA-256 해시 저장, TTL, 1회용). TTL 24시간. + +export const emailVerificationTokens = pgTable( + "email_verification_tokens", + { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + tokenHash: text("token_hash").notNull(), + expiresAt: timestamp("expires_at", { mode: "date", withTimezone: true, precision: 3 }).notNull(), + usedAt: timestamp("used_at", { mode: "date", withTimezone: true, precision: 3 }), + createdAt: timestamp("created_at", { mode: "date", withTimezone: true, precision: 3 }).notNull().defaultNow(), + }, + (t) => [index("email_verification_tokens_user_idx").on(t.userId), uniqueIndex("email_verification_tokens_hash_uidx").on(t.tokenHash)], +); + +export type EmailVerificationToken = typeof emailVerificationTokens.$inferSelect; + export type User = typeof users.$inferSelect; export type Credential = typeof credentials.$inferSelect; export type Identity = typeof identities.$inferSelect; diff --git a/src/lib/server/db/schema.sqlite.ts b/src/lib/server/db/schema.sqlite.ts index 3a9fc80..17dea4f 100644 --- a/src/lib/server/db/schema.sqlite.ts +++ b/src/lib/server/db/schema.sqlite.ts @@ -269,10 +269,8 @@ export const oidcGrants = sqliteTable( .notNull() .references(() => users.id, { onDelete: "cascade" }), sessionId: text("session_id").references(() => sessions.id, { onDelete: "set null" }), - // ctrls C-6: authorization code 평문 저장 제거. - // 신규 grant 는 codeHash (SHA-256) 만 저장. code 평문 컬럼은 legacy 호환 - // 위해 nullable 로 유지 — 다음 PR 에서 컬럼 자체 drop. - code: text("code"), + // ctrls C-6: authorization code 평문 저장 제거. 신규/기존 grant 모두 codeHash + // (SHA-256) 만 저장한다. (legacy code 평문 컬럼은 본 PR 에서 drop 완료.) codeHash: text("code_hash"), codeChallenge: text("code_challenge"), codeChallengeMethod: text("code_challenge_method", { enum: ["S256", "plain"] }), @@ -288,9 +286,7 @@ export const oidcGrants = sqliteTable( .default(sql`(unixepoch() * 1000)`), }, (t) => [ - // 기존 code 평문 unique 는 유지 (legacy grants 가 남아 있는 동안). NULL 다중 허용. - uniqueIndex("oidc_grants_code_uidx").on(t.code), - // codeHash unique — 신규 grant 의 1회용 invariant. NULL 다중 허용 (legacy row). + // codeHash unique — grant 의 1회용 invariant. NULL 다중 허용 (legacy row). uniqueIndex("oidc_grants_code_hash_uidx").on(t.codeHash), index("oidc_grants_tenant_client_idx").on(t.tenantId, t.clientId), index("oidc_grants_expires_idx").on(t.expiresAt), @@ -860,7 +856,9 @@ export const clientSkins = sqliteTable( fetchSecret: text("fetch_secret"), cacheTtlSeconds: integer("cache_ttl_seconds").notNull().default(3600), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), - createdAt: integer("created_at", { mode: "timestamp" }) + // ms 정밀도(timestamp_ms) 로 통일 — pg(precision 3)/mysql(fsp 3) 및 이 파일의 + // 다른 timestamp 컬럼과 동일 단위. (기존 초 단위 저장분은 마이그레이션에서 ×1000 보정.) + createdAt: integer("created_at", { mode: "timestamp_ms" }) .notNull() .$defaultFn(() => new Date()), }, @@ -890,6 +888,30 @@ export const passwordResetTokens = sqliteTable( export type PasswordResetToken = typeof passwordResetTokens.$inferSelect; +// ---------- Email Verification ---------- +// password_reset_tokens 와 동일 패턴(SHA-256 해시 저장, TTL, 1회용). TTL 24시간. + +export const emailVerificationTokens = sqliteTable( + "email_verification_tokens", + { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + tokenHash: text("token_hash").notNull(), + expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), + usedAt: integer("used_at", { mode: "timestamp_ms" }), + createdAt: integer("created_at", { mode: "timestamp_ms" }) + .notNull() + .default(sql`(unixepoch() * 1000)`), + }, + (t) => [index("email_verification_tokens_user_idx").on(t.userId), uniqueIndex("email_verification_tokens_hash_uidx").on(t.tokenHash)], +); + +export type EmailVerificationToken = typeof emailVerificationTokens.$inferSelect; + export type User = typeof users.$inferSelect; export type Credential = typeof credentials.$inferSelect; export type Identity = typeof identities.$inferSelect; diff --git a/src/lib/server/oidc/grant.ts b/src/lib/server/oidc/grant.ts index ae08503..4d46d99 100644 --- a/src/lib/server/oidc/grant.ts +++ b/src/lib/server/oidc/grant.ts @@ -34,14 +34,13 @@ async function sha256Base64Url(input: string): Promise { export async function createGrant(db: DB, params: CreateGrantParams): Promise { const codeHash = await sha256Base64Url(params.code); - // code (평문) 컬럼은 legacy 호환을 위해 nullable 로 유지하되, 신규 grant 는 - // null 로 저장한다. 다음 PR 에서 컬럼 자체를 drop. + // raw code(평문)는 DB 에 저장하지 않는다 — SHA-256 해시(codeHash)만 보관. + // params.code 는 해시 계산에만 쓰이므로 insert 대상 컬럼에서 제외한다. const { code: _omit, ...rest } = params; void _omit; await db.insert(oidcGrants).values({ id: crypto.randomUUID(), ...rest, - code: null, codeHash, expiresAt: new Date(Date.now() + AUTH_CODE_TTL_MS), }); From cad797c394cb88e2890bb6ea72c36f5fcb26edba Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 11:13:34 +0900 Subject: [PATCH 03/26] =?UTF-8?q?refactor(db):=20runAtomic=20=EA=B3=B5?= =?UTF-8?q?=EC=9A=A9=20=EC=9C=A0=ED=8B=B8=20=E2=80=94=20refresh=20?= =?UTF-8?q?=ED=9A=8C=EC=A0=84=20=EC=9B=90=EC=9E=90=ED=99=94=C2=B7=EC=84=9C?= =?UTF-8?q?=EB=AA=85=ED=82=A4=20=EC=BA=90=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - d1/sqlite=batch, pg/mysql=transaction 복붙 분기를 runAtomic()으로 추출 - refresh 토큰 회전을 claim+insert 원자 단위로 (claim 성공·insert 실패 시 세션 유실 창 제거, 동시 회전 패자 stray는 family 폐기로 무효화 — RFC 6819 §5.2.2.3) - getActiveSigningKey/getPublicJwks tenant별 globalThis 캐시(60s TTL, null 미캐시) + invalidateSigningKeyCache - totp enroll confirm도 runAtomic 치환(동작 무변경) Co-Authored-By: Claude Fable 5 --- src/lib/server/crypto/keys.ts | 77 ++++++++++++++++--- src/lib/server/db/atomic.ts | 50 ++++++++++++ src/lib/server/oidc/refresh.ts | 60 ++++++++++----- src/routes/api/totp/enroll/confirm/+server.ts | 19 +---- 4 files changed, 161 insertions(+), 45 deletions(-) create mode 100644 src/lib/server/db/atomic.ts diff --git a/src/lib/server/crypto/keys.ts b/src/lib/server/crypto/keys.ts index 06bc9fd..c38eaea 100644 --- a/src/lib/server/crypto/keys.ts +++ b/src/lib/server/crypto/keys.ts @@ -281,32 +281,89 @@ export async function decryptSecret(encrypted: string, masterSecret: string, con // ── DB helpers ──────────────────────────────────────────────────────────────── -export async function getActiveSigningKey( - db: DB, - tenantId: string, - secret: string, -): Promise<{ +/** + * 서명키/JWKS 단기 캐시 (task 2-4). + * + * getActiveSigningKey/getPublicJwks 는 모든 토큰 발급·JWKS 요청 경로에서 호출되어 + * 매번 DB 조회를 유발한다. tenant 별 globalThis isolate 캐시로 반복 조회를 절감한다 + * (bootstrap.ts 의 baseline 캐시와 동일 패턴). + * + * 한계(의도된 트레이드오프): + * - 캐시는 isolate 로컬이다. 서명키 회전/비활성화 액션은 같은 isolate 에서 + * invalidateSigningKeyCache 로 즉시 반영되지만, 다른 isolate 는 최대 TTL 만큼 + * 이전 활성 키를 계속 볼 수 있고 TTL 만료로 수렴한다. 회전은 드문 관리 작업이며 + * 짧은 TTL 로 이 수렴 창을 제한한다. + * - null(활성 키 없음)/빈 JWKS 는 캐시하지 않는다 — 최초 부트스트랩으로 키가 막 + * 생성된 직후에도 다음 호출이 곧바로 DB 를 다시 조회해 새 키를 반영하기 위함이다. + * - 캐시된 privateKey(CryptoKey)/publicJwk 는 메모리에만 머무르며 로그로 노출되지 + * 않는다. wrapped private key 원문은 캐시에 담지 않는다(복호화된 CryptoKey 만 보관). + * + * secret 은 isolate 수명 내 불변(env 유래)이므로 캐시 키에서 제외한다 — secret 이 + * 바뀌면 isolate 자체가 재시작되어 캐시가 사라진다. + */ +const SIGNING_KEY_CACHE_TTL_MS = 60 * 1000; // 60초 + +interface ActiveSigningKeyValue { kid: string; privateKey: CryptoKey; publicJwk: JsonWebKey; certPem: string | null; -} | null> { +} + +interface CacheEntry { + value: T; + expiresAt: number; +} + +// tenant 별 캐시 — 멀티테넌트 혼선 방지를 위해 tenantId 를 키로 사용. +const g = globalThis as typeof globalThis & { + __idpActiveSigningKeyCache?: Map>; + __idpPublicJwksCache?: Map>>>; +}; + +function activeSigningKeyCache(): Map> { + return (g.__idpActiveSigningKeyCache ??= new Map()); +} + +function publicJwksCache(): Map>>> { + return (g.__idpPublicJwksCache ??= new Map()); +} + +/** 서명키 회전/비활성화 후 호출 — 해당 tenant 의 서명키·JWKS 캐시를 즉시 무효화한다. */ +export function invalidateSigningKeyCache(tenantId: string): void { + activeSigningKeyCache().delete(tenantId); + publicJwksCache().delete(tenantId); +} + +export async function getActiveSigningKey(db: DB, tenantId: string, secret: string): Promise { + const cache = activeSigningKeyCache(); + const now = Date.now(); + const hit = cache.get(tenantId); + if (hit && hit.expiresAt > now) return hit.value; + const [row] = await db .select() .from(signingKeys) .where(and(eq(signingKeys.tenantId, tenantId), eq(signingKeys.active, true), isNull(signingKeys.rotatedAt))) .limit(1); - if (!row) return null; + if (!row) return null; // 활성 키 없음 → 캐시하지 않는다(키 생성 직후 즉시 반영). const privateKey = await unwrapPrivateKey(row.privateJwkEncrypted, secret); - return { + const value: ActiveSigningKeyValue = { kid: row.kid, privateKey, publicJwk: JSON.parse(row.publicJwk) as JsonWebKey, certPem: row.certPem ?? null, }; + cache.set(tenantId, { value, expiresAt: now + SIGNING_KEY_CACHE_TTL_MS }); + return value; } export async function getPublicJwks(db: DB, tenantId: string): Promise>> { + const cache = publicJwksCache(); + const now = Date.now(); + const hit = cache.get(tenantId); + if (hit && hit.expiresAt > now) return hit.value; + // 회전 또는 비활성 처리된 키는 JWKS 에 노출하지 않는다. const rows = await db .select({ @@ -317,10 +374,12 @@ export async function getPublicJwks(db: DB, tenantId: string): Promise ({ + const value = rows.map((r) => ({ ...(JSON.parse(r.publicJwk) as Record), kid: r.kid, use: r.use, alg: r.alg, })); + if (value.length > 0) cache.set(tenantId, { value, expiresAt: now + SIGNING_KEY_CACHE_TTL_MS }); // 빈 JWKS 는 캐시하지 않는다. + return value; } diff --git a/src/lib/server/db/atomic.ts b/src/lib/server/db/atomic.ts new file mode 100644 index 0000000..0a8d170 --- /dev/null +++ b/src/lib/server/db/atomic.ts @@ -0,0 +1,50 @@ +/** + * 방언 무관 원자적 다중 write 유틸. + * + * drizzle 은 방언마다 "여러 문장을 원자적으로 실행"하는 API 가 다르다: + * - d1 / sqlite(libSQL): interactive transaction 미지원 → `db.batch([...])`. + * 빌더 배열을 받아 하나의 batch(트랜잭션)로 실행하며, 중간 문장이 실패하면 전체 + * rollback 된다(부분 적용 없음). + * - postgres / mysql: interactive transaction → `db.transaction(cb)`. + * 콜백에서 문장을 순차 await 하며, 콜백이 throw 하면 전체 rollback 된다. + * + * `runAtomic` 은 이 차이(배열 vs 콜백)를 흡수한다. 호출부는 executor 를 받아 drizzle + * write 빌더를 반환하는 op 배열만 넘기면 되고, 두 경로 모두 동일한 원자성·순서 보장을 + * 받는다. 반환값은 각 op 의 실행 결과(예: `.returning()` rows, mysql `affectedRows`)를 + * op 순서대로 담은 배열이다 — 결과가 필요 없는 호출부는 무시하면 된다. + */ + +import { type DB, DB_DIALECT } from "$lib/server/db"; + +/** + * op 이 받는 실행자. 전체 `db`(batch 경로) 또는 transaction 핸들(transaction 경로)이 + * 전달되며, 둘 다 write 빌더 진입점을 제공한다. + */ +export type AtomicExecutor = Pick; + +/** + * 원자 단위로 실행할 단일 write. executor 를 받아 drizzle 빌더를 반환한다. + * batch 경로에서는 반환된 빌더가 배열로 수집되고, transaction 경로에서는 순차 await 된다. + */ +export type AtomicOp = (h: AtomicExecutor) => unknown; + +/** + * ops 를 활성 방언에 맞는 원자 단위로 실행한다. 결과 배열은 op 순서와 일치한다. + * 두 경로 모두 중간 실패 시 전체 rollback 되므로 부분 적용이 발생하지 않는다. + */ +export async function runAtomic(db: DB, ops: AtomicOp[]): Promise { + if (DB_DIALECT === "d1" || DB_DIALECT === "sqlite") { + // 정규 DB 타입에 batch 가 노출되지 않는 방언 조합이 있어 캐스팅. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const builders = ops.map((op) => op(db as unknown as AtomicExecutor)) as any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (await (db as any).batch(builders)) as unknown[]; + } + // postgres / mysql: interactive transaction. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (await (db as any).transaction(async (tx: AtomicExecutor) => { + const results: unknown[] = []; + for (const op of ops) results.push(await op(tx)); + return results; + })) as unknown[]; +} diff --git a/src/lib/server/oidc/refresh.ts b/src/lib/server/oidc/refresh.ts index 0db1526..9f7cfad 100644 --- a/src/lib/server/oidc/refresh.ts +++ b/src/lib/server/oidc/refresh.ts @@ -13,6 +13,7 @@ import { and, eq, isNull, lt } from "drizzle-orm"; import { type DB, DB_DIALECT } from "$lib/server/db"; +import { runAtomic } from "$lib/server/db/atomic"; import { oidcRefreshTokens } from "$lib/server/db/schema"; export type OidcRefreshTokenRecord = typeof oidcRefreshTokens.$inferSelect; @@ -141,36 +142,53 @@ export async function rotateRefreshToken(db: DB, tenantId: string, clientId: str const newToken = generateRefreshToken(); const newHash = await hashRefreshToken(newToken); - // old 토큰을 원자적으로 claim (revokedAt IS NULL 가드). 동시 사용은 하나만 성공. + // old 토큰 claim(revokedAt IS NULL 가드) 과 new 토큰 insert 를 하나의 원자 단위로 실행한다. + // + // 동시성 시맨틱: claim UPDATE 가 실제로 한 행을 revoke 했는가(비-mysql RETURNING rows, + // mysql affectedRows)가 동시 회전 경쟁의 승자를 결정한다 — revokedAt IS NULL 가드로 + // 동시 요청 중 단 하나만 claim 에 성공한다. (기존 시맨틱 그대로 보존) + // + // 원자성: claim 과 insert 를 같은 batch/transaction 에 묶어 부분 적용을 제거한다. + // insert 가 실패하면 claim 도 함께 rollback 되므로, 기존에 존재하던 "claim 성공 + + // insert 실패 → old-revoked·new-부재(세션 유실)" 창이 사라진다. 이 창이 닫히는 것은 + // batch(d1/sqlite)·transaction(pg/mysql) 모두 중간 실패 시 전체 rollback 하기 때문이다. + // + // 경쟁 패자 처리: batch 는 앞 문장 결과로 뒤 문장을 조건 분기할 수 없으므로 insert 는 + // 항상 실행된다. 패자(claim 0행)의 요청도 new 토큰을 삽입하지만, 이 raw 값은 호출자에게 + // 반환되지 않고(never handed out) 곧바로 아래 family 폐기로 승자 토큰과 함께 무효화된다 + // — 재사용 감지 시맨틱(RFC 6819 §5.2.2.3)과 결과가 동일하다. const claimWhere = and(eq(oidcRefreshTokens.id, record.id), isNull(oidcRefreshTokens.revokedAt)); - let claimed: boolean; - if (DB_DIALECT === "mysql") { - const res = (await db.update(oidcRefreshTokens).set({ revokedAt: now, replacedById: newId }).where(claimWhere)) as unknown as [{ affectedRows: number }]; - claimed = Boolean(res?.[0]?.affectedRows); - } else { + const buildClaim = (h: Pick) => { + const builder = h.update(oidcRefreshTokens).set({ revokedAt: now, replacedById: newId }).where(claimWhere); + // mysql 은 UPDATE ... RETURNING 미지원 → affectedRows 로 승자 판정. 그 외 방언은 RETURNING. // eslint-disable-next-line @typescript-eslint/no-explicit-any - const updateBuilder = db.update(oidcRefreshTokens).set({ revokedAt: now, replacedById: newId }).where(claimWhere) as any; - const rows = (await updateBuilder.returning({ id: oidcRefreshTokens.id })) as Array<{ id: string }>; - claimed = rows.length > 0; - } + return DB_DIALECT === "mysql" ? builder : (builder as any).returning({ id: oidcRefreshTokens.id }); + }; + const buildInsert = (h: Pick) => + h.insert(oidcRefreshTokens).values({ + id: newId, + tenantId, + clientId, + userId: record.userId, + sessionId: record.sessionId, + tokenHash: newHash, + scope: record.scope, + expiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL_MS), + }); + + // claim 이 index 0 → 결과 배열 첫 항목이 claim 결과. insert 실패는 여기서 throw 되어 + // (rollback 후) 호출자로 전파된다 — old 토큰은 원복되어 재시도 가능하다. + const [claimResult] = await runAtomic(db, [buildClaim, buildInsert]); + const claimed = + DB_DIALECT === "mysql" ? Boolean((claimResult as [{ affectedRows: number }] | undefined)?.[0]?.affectedRows) : ((claimResult as Array<{ id: string }> | undefined) ?? []).length > 0; if (!claimed) { // 이미 다른 요청이 회전 → 동시 재사용으로 간주하고 family 폐기. + // (위 insert 로 삽입된 new 토큰도 활성 상태이므로 family 폐기에 함께 무효화된다.) await revokeRefreshTokenFamily(db, tenantId, record.userId, clientId); return { ok: false, reason: "reuse" }; } - await db.insert(oidcRefreshTokens).values({ - id: newId, - tenantId, - clientId, - userId: record.userId, - sessionId: record.sessionId, - tokenHash: newHash, - scope: record.scope, - expiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL_MS), - }); - return { ok: true, record, newToken }; } diff --git a/src/routes/api/totp/enroll/confirm/+server.ts b/src/routes/api/totp/enroll/confirm/+server.ts index 04ac5c8..868bc16 100644 --- a/src/routes/api/totp/enroll/confirm/+server.ts +++ b/src/routes/api/totp/enroll/confirm/+server.ts @@ -6,7 +6,8 @@ import { requireDbContext } from "$lib/server/auth/guards"; import { encryptTotpSecret, generateBackupCodes, hashBackupCode, verifyTotp } from "$lib/server/auth/totp"; import { checkRateLimit } from "$lib/server/ratelimit"; import { credentials, users } from "$lib/server/db/schema"; -import { type DB, DB_DIALECT } from "$lib/server/db"; +import type { DB } from "$lib/server/db"; +import { runAtomic } from "$lib/server/db/atomic"; import { isUniqueViolation } from "$lib/server/db/errors"; /** @@ -74,8 +75,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { // - totpOwnerId 에 userId 를 채워 credentials_totp_owner_uidx (unique) 가 // 사용자당 TOTP 1개를 DB 레벨에서 강제 → 동시 confirm 두 건 중 하나만 성공. // - 백업코드 등 다른 INSERT 는 totpOwnerId 미설정(NULL) 이므로 unique 검사 제외. - // - d1/sqlite: db.batch (interactive transaction 미지원). - // - postgres/mysql: interactive transaction. + // runAtomic 이 d1/sqlite=batch, postgres/mysql=transaction 분기를 흡수한다. const buildTotpInsert = (h: Pick) => h.insert(credentials).values({ id: crypto.randomUUID(), @@ -90,18 +90,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { const buildBackupInsert = (h: Pick) => h.insert(credentials).values(backupCodeRows); try { - if (DB_DIALECT === "d1" || DB_DIALECT === "sqlite") { - // d1 / libSQL 은 원자적 batch 지원 (정규 DB 타입이 활성 방언일 때만 노출되므로 캐스팅). - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (db as any).batch([buildTotpInsert(db), buildBackupInsert(db)]); - } else { - // postgres / mysql: interactive transaction. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (db as any).transaction(async (tx: Pick) => { - await buildTotpInsert(tx); - await buildBackupInsert(tx); - }); - } + await runAtomic(db, [buildTotpInsert, buildBackupInsert]); } catch (err) { // credentials_totp_owner_uidx UNIQUE 위반(동시 이중 등록)일 때만 409 로 매핑한다. // 사전 SELECT 를 통과한 두 동시 요청 중 두 번째가 여기서 안전하게 거부된다. From dd145add835029d38d84bcdc032bffce5fe48e5a Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 11:13:35 +0900 Subject: [PATCH 04/26] =?UTF-8?q?feat(gc):=20=EB=A7=8C=EB=A3=8C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20GC=20=EB=8F=84=EC=9E=85=20?= =?UTF-8?q?=E2=80=94=2010=EC=A2=85=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EB=AC=B4=ED=95=9C=20=EC=84=B1=EC=9E=A5=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - runExpiredDataGc: sessions(만료+30일 유예)·oidc_grants·refresh/reset/email 토큰·webauthn challenges·rate_limits·saml 3종 일괄 purge, 테이블별 에러 격리 - sessions 유예 30일 근거: revoke-on-logout 불변식(폐기 세션의 refresh 토큰은 전부 revoke) — 조기 삭제로 인한 세션 검사 우회 방지 - Workers: 요청 1% 확률 waitUntil 발사(adapter-cloudflare가 scheduled 미지원이라 cron 대신), Node: 1시간 setInterval(unref+중복 가드) - orphan이던 기존 purge 3함수 호출 연결 Co-Authored-By: Claude Fable 5 --- src/hooks.server.ts | 13 +++ src/lib/server/db/gc.ts | 252 ++++++++++++++++++++++++++++++++++++++++ wrangler.example.jsonc | 8 ++ 3 files changed, 273 insertions(+) create mode 100644 src/lib/server/db/gc.ts diff --git a/src/hooks.server.ts b/src/hooks.server.ts index b5125d1..52411c7 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -4,6 +4,7 @@ import { SESSION_COOKIE_NAME, SESSION_TOUCH_INTERVAL_MS } from "$lib/server/auth import { getRuntimeConfig } from "$lib/server/auth/runtime"; import { clearSessionCookie, getSessionContext, touchSession } from "$lib/server/auth/session"; import { getDb, DB_DIALECT } from "$lib/server/db"; +import { ensureNodeGcScheduler, maybeRunWorkersGc } from "$lib/server/db/gc"; import { LOCALE_COOKIE_NAME, resolveLocale } from "$lib/server/locale"; // CSRF: state-changing 요청에 대해 same-origin을 강제할 라우트 @@ -98,6 +99,18 @@ export const handle: Handle = async ({ event, resolve }) => { const db = handle.db; disposeDb = handle.dispose; event.locals.db = db; + + // 만료 데이터 GC 실행 경로 (요청 처리와 완전히 격리 — 실패해도 무영향): + // - Workers: 요청의 ~1% 에서 ctx.waitUntil 로 백그라운드 발사(응답 지연 0). + // - Node: 최초 1회 setInterval(1시간) 스케줄러 기동(중복 가드 내장). + // adapter-cloudflare 는 커스텀 worker 엔트리 없이 scheduled() Cron 을 노출할 수 + // 없어(생성된 _worker.js 는 fetch 만 export) 빌드 침습을 피해 확률적 GC 를 택했다. + if (typeof event.platform?.ctx?.waitUntil === "function") { + maybeRunWorkersGc(event.platform); + } else { + ensureNodeGcScheduler(); + } + event.locals.tenant = skipBaseline ? null : await ensureAuthBaseline(db, event.platform); const sessionToken = event.cookies.get(SESSION_COOKIE_NAME); diff --git a/src/lib/server/db/gc.ts b/src/lib/server/db/gc.ts new file mode 100644 index 0000000..81cfd9a --- /dev/null +++ b/src/lib/server/db/gc.ts @@ -0,0 +1,252 @@ +/** + * 만료 데이터 GC (Garbage Collection) + * + * 만료·소진된 행이 무한히 쌓이는 테이블을 주기적으로 정리한다. 정의만 되어 있고 + * 호출부가 없던 purge 함수 3개(webauthn 챌린지 / refresh token / rate limit)를 + * 통합 호출하고, purge 함수조차 없던 나머지 테이블은 여기서 직접 만료 DELETE 한다. + * + * 설계 원칙 — **미만료·미소진 데이터는 절대 삭제하지 않는다.** + * - 각 테이블의 만료 판정 컬럼/시맨틱을 스키마·사용처 코드에서 확인해 보수적으로 삭제. + * - 테이블별 에러 격리: 하나가 실패해도 나머지는 계속 진행한다. + * - 삭제 건수는 best-effort 로 로깅한다(방언별 결과 형태가 달라 미상일 수 있음). + * + * 실행 경로(두 런타임): + * - Cloudflare Workers: adapter-cloudflare 는 커스텀 worker 엔트리 없이는 `scheduled()` + * 핸들러(Cron Trigger)를 노출할 수 없다(생성된 `_worker.js` 는 fetch 만 export). 빌드 + * 구조를 바꾸는 것은 과침습이므로, 요청의 ~1% 에서 `ctx.waitUntil` 로 GC 를 백그라운드 + * 발사한다(응답 지연 0). → maybeRunWorkersGc() + * - Node(adapter-node): 프로세스가 장수하므로 setInterval(1시간, unref) 로 주기 실행하고 + * globalThis 플래그로 중복 기동을 막는다. → ensureNodeGcScheduler() + * + * GC 실패는 요청 처리에 절대 영향을 주지 않는다(전부 try/catch + waitUntil 격리). + */ + +import { lt, or } from "drizzle-orm"; +import { getDb, DB_DIALECT, type DB } from "./index"; +import { sessions, oidcGrants, passwordResetTokens, emailVerificationTokens, samlSloStates, samlAuthnRequestIds, samlSessions } from "./schema"; +import { purgeExpiredChallenges } from "$lib/server/auth/webauthn"; +import { purgeExpiredRefreshTokens, REFRESH_TOKEN_TTL_MS } from "$lib/server/oidc/refresh"; +import { purgeExpiredRateLimits } from "$lib/server/ratelimit"; +import { SESSION_TTL_MS } from "$lib/server/auth/constants"; + +// ── 유예(grace) 상수 ───────────────────────────────────────────────────────────── + +/** + * sessions 삭제 유예 = refresh token TTL(30일). 단, 이 값은 안전성의 **근거가 아니라 + * 보수적 버퍼**다 — 실제 안전성은 아래 revoke-on-logout 불변식이 보장한다. + * + * 문제 표면: 세션 row 가 삭제되면 refresh token/grant 의 sessionId 가 FK + * `onDelete: set null` 로 NULL 이 되고, 토큰 엔드포인트는 sessionId 가 NULL 이면 연결 + * 세션의 폐기/만료 검사를 건너뛴다. 따라서 "성급히 삭제된 세션의 토큰이 되살아나는" 우회를 + * 막아야 한다. + * + * "토큰 TTL 산술"(토큰 expiresAt ≤ session.expiresAt + 30일)은 이 안전성의 엄밀한 근거가 + * 아니다: 자연 만료(expired)된 세션은 refresh 토큰 회전을 막지 않으므로, 세션이 자연 만료된 + * 채로 남아 있어도 우회 자체가 문제되지 않고, 반대로 산술만으로 우회 부재를 증명할 수도 없다. + * + * 실제 안전 근거(revoke-on-logout 불변식): 우회가 위험한 경우는 **폐기(revoked)** 세션뿐인데, + * 로그아웃/강제 종료 시 세션 폐기와 **동시에** 그 세션에 묶인 refresh token 이 전부 revoke + * 된다(logout 플로우의 revokeSession + revokeRefreshTokensForSession). 그리고 + * rotateRefreshToken 은 `record.revokedAt` 이 설정된 토큰을 sessionId 가 NULL 인지와 + * 무관하게 거부한다. 즉 GC 가 폐기 세션을 일찍 삭제해 sessionId 를 NULL 로 만들어도, 그 토큰은 + * 이미 revoked 이므로 계속 거부된다 — 세션 검사 우회로 되살아나지 않는다. 30일 유예는 이 불변식 + * 위에 얹는 여유일 뿐 정확성 요건은 아니다. + */ +const SESSION_GC_GRACE_MS = REFRESH_TOKEN_TTL_MS; + +/** + * saml_sessions 삭제 유예 = IdP 세션 TTL(12시간). + * + * SLO 체인은 `endedAt IS NULL` 로 활성 SAML 세션을 조회한다(notOnOrAfter 무관). + * 만료(notOnOrAfter 경과)됐거나 로그아웃(endedAt 설정)된 SAML 세션이라도, 부모 IdP + * 세션이 살아있는 동안 진행 중인 SLO 체인에서 참조될 수 있다. 부모 IdP 세션 TTL(12h) + * 만큼 유예를 두면 부모 세션도 확실히 만료된 뒤에만 삭제되어 SLO 일관성을 해치지 않는다. + */ +const SAML_SESSION_GC_GRACE_MS = SESSION_TTL_MS; + +// ── 실행 주기/샘플링 ───────────────────────────────────────────────────────────── + +/** Node setInterval 주기(1시간). */ +const NODE_GC_INTERVAL_MS = 60 * 60 * 1000; + +/** Workers 확률적 GC 발사 비율(요청의 ~1%). */ +const WORKERS_GC_SAMPLE_RATE = 0.01; + +// ── 결과 타입 ──────────────────────────────────────────────────────────────────── + +export interface GcTableResult { + table: string; + /** 삭제 행 수. 방언별 결과 형태로 산출 불가하면 null. */ + deleted: number | null; + ok: boolean; + error?: string; +} + +export interface GcResult { + startedAt: number; + durationMs: number; + tables: GcTableResult[]; +} + +/** + * 방언별 DELETE 결과에서 영향받은 행 수를 best-effort 로 추출한다. + * d1: meta.changes / libsql: rowsAffected / postgres-js: count / mysql2: [header].affectedRows. + * 어느 것도 아니면 null(미상). + */ +function extractAffected(res: unknown): number | null { + if (res == null) return null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const r = res as any; + if (typeof r.rowsAffected === "number") return r.rowsAffected; // libsql + if (r.meta && typeof r.meta.changes === "number") return r.meta.changes; // d1 + if (typeof r.count === "number") return r.count; // postgres-js + if (typeof r.affectedRows === "number") return r.affectedRows; // mysql2 (일부 경로) + if (Array.isArray(r) && r[0] && typeof r[0].affectedRows === "number") return r[0].affectedRows; // mysql2 + return null; +} + +// ── 통합 GC ────────────────────────────────────────────────────────────────────── + +/** + * 만료 데이터를 정리한다. 테이블별 에러 격리 + 삭제 건수 로깅. + * GC 는 조회 성능·저장공간을 위한 것이며, 어떤 만료 판정도 **인증 쿼리와 동일하거나 더 + * 보수적인 조건**만 사용한다(미만료·미소진 행 보존이 최우선). + */ +export async function runExpiredDataGc(db: DB): Promise { + const startedAt = Date.now(); + const now = new Date(); + const tables: GcTableResult[] = []; + + // 각 정리 단계를 개별 격리 실행. purge 함수(void 반환)는 건수 미상(null). + const runPurge = async (table: string, fn: () => Promise) => { + try { + await fn(); + tables.push({ table, deleted: null, ok: true }); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + console.error(`[gc] purge ${table} 실패:`, msg); + tables.push({ table, deleted: null, ok: false, error: msg }); + } + }; + + const runDelete = async (table: string, exec: () => Promise) => { + try { + const res = await exec(); + const deleted = extractAffected(res); + tables.push({ table, deleted, ok: true }); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + console.error(`[gc] delete ${table} 실패:`, msg); + tables.push({ table, deleted: null, ok: false, error: msg }); + } + }; + + // 1) refresh token 을 먼저 purge — 만료 토큰을 제거해 아래 sessions 삭제 시 FK set-null + // 대상(살아있는 토큰)이 없음을 확실히 한다. + await runPurge("oidc_refresh_tokens", () => purgeExpiredRefreshTokens(db)); + // 2) 기존 purge 함수들 (import 호출) + await runPurge("webauthn_challenges", () => purgeExpiredChallenges(db)); + await runPurge("rate_limits", () => purgeExpiredRateLimits(db)); + + // 3) 직접 만료 DELETE ───────────────────────────────────────────────────────── + const sessionCutoff = new Date(now.getTime() - SESSION_GC_GRACE_MS); + const samlSessionCutoff = new Date(now.getTime() - SAML_SESSION_GC_GRACE_MS); + + // oidc_grants: authorization code(수 분 TTL). expiresAt 경과 시 삭제(미사용·소진 무관 — + // 소진(usedAt) 여부와 상관없이 만료된 grant 는 재사용 불가하며 5분 창을 넘기면 무의미). + await runDelete("oidc_grants", () => db.delete(oidcGrants).where(lt(oidcGrants.expiresAt, now))); + + // password_reset_tokens: expiresAt 경과 시 삭제. 사용됨(usedAt)·미사용 모두 만료 후엔 무효. + await runDelete("password_reset_tokens", () => db.delete(passwordResetTokens).where(lt(passwordResetTokens.expiresAt, now))); + + // email_verification_tokens: expiresAt 경과 시 삭제. 사용됨·미사용 모두 만료 후엔 무효. + await runDelete("email_verification_tokens", () => db.delete(emailVerificationTokens).where(lt(emailVerificationTokens.expiresAt, now))); + + // saml_slo_states: SLO 체인 상태. 자체 expiresAt 경과 시 삭제(만료된 체인은 죽은 상태). + await runDelete("saml_slo_states", () => db.delete(samlSloStates).where(lt(samlSloStates.expiresAt, now))); + + // saml_authn_request_ids: AuthnRequest ID replay 캐시. **반드시 expiresAt 이후에만** 삭제 + // (조기 삭제 시 replay 창이 다시 열림). 만료된 요청 ID 는 재생 방어 대상이 아니므로 안전. + await runDelete("saml_authn_request_ids", () => db.delete(samlAuthnRequestIds).where(lt(samlAuthnRequestIds.expiresAt, now))); + + // saml_sessions: notOnOrAfter(SAML 세션 유효창, 8h) 또는 endedAt(로그아웃)이 유예(12h)를 + // 넘겨 지난 경우만 삭제. endedAt IS NULL 인 활성 세션은 notOnOrAfter 분기로만 잡힌다 + // (NULL 비교는 참이 아니므로 활성 세션이 성급히 삭제되지 않는다). + await runDelete("saml_sessions", () => db.delete(samlSessions).where(or(lt(samlSessions.notOnOrAfter, samlSessionCutoff), lt(samlSessions.endedAt, samlSessionCutoff)))); + + // sessions: expiresAt 이 refresh TTL(30일) 유예를 넘겨 지난 경우만 삭제. 위 SESSION_GC_GRACE_MS + // 주석의 근거대로, 이 시점엔 세션에 묶인 모든 refresh token 이 만료(및 purge)돼 있어 + // FK set-null 로 인한 세션 검사 우회가 발생하지 않는다. + await runDelete("sessions", () => db.delete(sessions).where(lt(sessions.expiresAt, sessionCutoff))); + + const result: GcResult = { startedAt, durationMs: Date.now() - startedAt, tables }; + + const totalDeleted = tables.reduce((sum, t) => sum + (t.deleted ?? 0), 0); + const failed = tables.filter((t) => !t.ok).map((t) => t.table); + console.log(`[gc] 완료 ${result.durationMs}ms — 삭제 ${totalDeleted}+ 건` + (failed.length ? `, 실패 테이블: ${failed.join(", ")}` : ""), tables); + + return result; +} + +// ── 실행 경로: Workers(확률적) ──────────────────────────────────────────────────── + +/** + * Cloudflare Workers 요청 훅에서 호출. 확률적으로(~1%) GC 를 백그라운드 발사한다. + * - `platform.ctx.waitUntil` 이 없으면(=Workers 아님) no-op. + * - 요청 응답을 지연시키지 않는다(waitUntil 백그라운드). + * - 요청 DB 연결의 생명주기(dispose)와 얽히지 않도록 GC 전용 연결을 새로 열고 닫는다. + * - GC 실패는 요청 처리에 영향 없음(내부에서 전부 catch). + */ +export function maybeRunWorkersGc(platform: App.Platform | undefined): void { + const waitUntil = platform?.ctx?.waitUntil?.bind(platform.ctx); + if (!waitUntil) return; // Workers 아님 + if (Math.random() >= WORKERS_GC_SAMPLE_RATE) return; + + waitUntil( + (async () => { + let dispose: (() => Promise) | undefined; + try { + const handle = await getDb(platform); + dispose = handle.dispose; + await runExpiredDataGc(handle.db); + } catch (error) { + console.error("[gc] Workers GC 실행 실패:", error); + } finally { + if (dispose) await dispose().catch((e) => console.error("[gc] GC 연결 정리 실패:", e)); + } + })(), + ); +} + +// ── 실행 경로: Node(setInterval) ────────────────────────────────────────────────── + +declare global { + var __keystoneGcTimer: ReturnType | undefined; +} + +/** + * Node(adapter-node) 서버에서 호출. 최초 1회만 1시간 간격 GC 타이머를 건다. + * - globalThis 플래그로 중복 기동을 막는다(HMR/다중 import 안전). + * - `unref()` 로 이 타이머가 프로세스 종료를 막지 않게 한다. + * - DB_DIALECT="d1" 은 Workers 전용이라 Node 경로에선 스케줄하지 않는다. + * - 매 tick 마다 getDb(undefined)(Node 전역 재사용 연결)로 GC 를 돌린다. + */ +export function ensureNodeGcScheduler(): void { + if (globalThis.__keystoneGcTimer) return; + if (DB_DIALECT === "d1") return; // d1 은 Workers 전용 — Node 스케줄 불가 + + const timer = setInterval(() => { + void (async () => { + try { + const { db } = await getDb(undefined); + await runExpiredDataGc(db); + } catch (error) { + console.error("[gc] Node GC 실행 실패:", error); + } + })(); + }, NODE_GC_INTERVAL_MS); + + // 프로세스 종료를 막지 않도록 unref (테스트/CLI 환경 안전). + timer.unref?.(); + globalThis.__keystoneGcTimer = timer; +} diff --git a/wrangler.example.jsonc b/wrangler.example.jsonc index 094469f..9054235 100644 --- a/wrangler.example.jsonc +++ b/wrangler.example.jsonc @@ -22,6 +22,14 @@ "enabled": true, "head_sampling_rate": 1, }, + // ── 만료 데이터 GC ───────────────────────────────────────────────────────── + // 만료·소진 행(sessions/oidc_grants/refresh_tokens/webauthn_challenges/rate_limits/ + // password_reset_tokens/saml_* 등)은 별도 Cron 없이 정리된다: + // 요청의 ~1% 에서 ctx.waitUntil 로 GC 를 백그라운드 발사(src/lib/server/db/gc.ts). + // adapter-cloudflare 가 생성하는 _worker.js 는 fetch 핸들러만 export 하므로 Cron + // Trigger(scheduled 핸들러)를 커스텀 worker 엔트리 없이는 붙일 수 없어 확률적 GC 를 + // 택했다. 정기 Cron 이 필요하면 커스텀 엔트리로 scheduled() 를 추가하고 아래를 켜라: + // "triggers": { "crons": ["0 * * * *"] } // 매시 정각, scheduled()->runExpiredDataGc "vars": { // ── DB 방언 선택 (배포 단위, 하나만 사용) ────────────────────────────── // "d1"(기본) | "postgres" | "mysql". From 430bda9587dd0ddce18ebe0f49e995e1bdb871c5 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 11:14:32 +0900 Subject: [PATCH 05/26] =?UTF-8?q?feat(email):=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=20=ED=94=8C=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?+=20=EB=B3=B4=EC=95=88=20=EC=95=8C=EB=A6=BC=20=EB=A9=94?= =?UTF-8?q?=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 가입 시 인증 메일(SHA-256 해시 저장·24h TTL·rate-limit) → verify-email 라우트(GET 비소진, POST 원자 소진) → emailVerifiedAt 세팅으로 email_verified 클레임 정상화(기존엔 항상 false) - 재발송 액션 + 프로필 미인증 배너 (프로필 저장은 default→named save 액션 전환) - 보안 알림 9종(비번 변경/재설정·TOTP 등록/삭제/백업코드·패스키 등록/삭제·admin 초기화/잠금) — waitUntil best-effort 격리, users.locale 기반 ko/en - 발송 실패는 본 동작에 영향 없음, 이메일 없는 계정 스킵 Co-Authored-By: Claude Fable 5 --- src/lib/server/auth/email-verification.ts | 35 ++++++++ src/lib/server/email.ts | 43 ++++++++++ src/lib/server/security-notify.ts | 54 +++++++++++++ .../(auth)/reset-password/+page.server.ts | 7 +- src/routes/(auth)/signup/+page.server.ts | 4 + .../(auth)/verify-email/+page.server.ts | 81 +++++++++++++++++++ src/routes/(auth)/verify-email/+page.svelte | 50 ++++++++++++ src/routes/account/mfa/+page.server.ts | 7 ++ src/routes/account/passkeys/+page.server.ts | 3 + src/routes/account/profile/+page.server.ts | 28 ++++++- src/routes/account/profile/+page.svelte | 26 +++++- .../api/webauthn/register/verify/+server.ts | 3 + 12 files changed, 338 insertions(+), 3 deletions(-) create mode 100644 src/lib/server/auth/email-verification.ts create mode 100644 src/lib/server/security-notify.ts create mode 100644 src/routes/(auth)/verify-email/+page.server.ts create mode 100644 src/routes/(auth)/verify-email/+page.svelte diff --git a/src/lib/server/auth/email-verification.ts b/src/lib/server/auth/email-verification.ts new file mode 100644 index 0000000..e3d2c98 --- /dev/null +++ b/src/lib/server/auth/email-verification.ts @@ -0,0 +1,35 @@ +import { env } from "$env/dynamic/private"; +import type { DB } from "$lib/server/db"; +import { emailVerificationTokens } from "$lib/server/db/schema"; +import { generateToken, sendEmailVerificationEmail } from "$lib/server/email"; + +export const EMAIL_VERIFY_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24시간 + +/** + * F1: 이메일 인증 토큰 발급 + 인증 메일 발송(공통 — 가입/재발송에서 사용). + * 발송 실패가 상위 흐름(가입/재발송 응답)을 실패시키지 않도록 완전 격리한다: + * - issuer(IDP_ISSUER_URL) 미설정 시 발송 스킵(host header injection 방지 목적). + * - Workers 는 waitUntil 로 응답 경로에서 분리, Node 는 await. + * - 토큰 발급/발송 예외는 삼켜서 호출부에 전파하지 않는다. + */ +export async function issueEmailVerification(db: DB, userId: string, email: string, platform: App.Platform | undefined): Promise { + const issuer = env.IDP_ISSUER_URL?.replace(/\.+$/, "").replace(/\/+$/, ""); + if (!issuer) { + console.error("[email-verification] IDP_ISSUER_URL 미설정 — 이메일 인증 메일 발송 불가"); + return; + } + try { + const { token, tokenHash } = await generateToken(); + const expiresAt = new Date(Date.now() + EMAIL_VERIFY_EXPIRY_MS); + await db.insert(emailVerificationTokens).values({ userId, tokenHash, expiresAt }); + const verifyUrl = `${issuer}/verify-email?token=${encodeURIComponent(token)}`; + const sendPromise = sendEmailVerificationEmail(email, verifyUrl, platform).catch(() => { + // 발송 실패는 조용히 무시 + }); + const wait = platform?.ctx?.waitUntil?.bind(platform.ctx); + if (wait) wait(sendPromise); + else await sendPromise; + } catch { + // 토큰 발급/발송 실패가 상위 흐름을 실패시키지 않도록 격리 + } +} diff --git a/src/lib/server/email.ts b/src/lib/server/email.ts index e0007ad..e9a66ac 100644 --- a/src/lib/server/email.ts +++ b/src/lib/server/email.ts @@ -160,6 +160,49 @@ export async function sendPasswordResetEmail(to: string, resetUrl: string, platf await send(to, "비밀번호 재설정 안내", html, text, platform); } +export async function sendEmailVerificationEmail(to: string, verifyUrl: string, platform?: App.Platform): Promise { + const safeUrl = safeAbsoluteUrl(verifyUrl); + if (!safeUrl) { + // 잘못된 URL 형식이면 메일 발송 자체 거부. + console.error("[email] sendEmailVerificationEmail: 잘못된 verifyUrl scheme — 발송 취소"); + return; + } + const html = baseHtml( + "이메일 인증", + `

아래 버튼을 클릭하여 이메일 주소를 인증하세요. 링크는 24시간 동안 유효합니다.

+

+ 이메일 인증 +

`, + ); + const text = `아래 링크에서 이메일 주소를 인증하세요. 링크는 24시간 동안 유효합니다.\n\n${safeUrl}\n\n본인이 요청하지 않았다면 이 이메일을 무시해 주세요.`; + await send(to, "이메일 인증 안내", html, text, platform); +} + +// ── 보안 알림 메일 (best-effort) ───────────────────────────────────────────── +// 문구는 호출부에서 수신자 locale 로 번역해 넘긴다(email.ts 는 i18n 에 결합하지 않는다). +// baseHtml 의 고정 한국어 푸터 대신 전달받은 localized 문구로 본문을 구성한다. +export interface SecurityAlertContent { + subject: string; + heading: string; + body: string; + whenText: string; + footer: string; +} + +export async function sendSecurityAlertEmail(to: string, content: SecurityAlertContent, platform?: App.Platform): Promise { + const html = ` + + +

${escapeHtml(content.heading)}

+

${escapeHtml(content.body)}

+

${escapeHtml(content.whenText)}

+

${escapeHtml(content.footer)}

+ +`; + const text = `${content.heading}\n\n${content.body}\n${content.whenText}\n\n${content.footer}`; + await send(to, content.subject, html, text, platform); +} + export async function generateToken(): Promise<{ token: string; tokenHash: string }> { const bytes = crypto.getRandomValues(new Uint8Array(32)); const token = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); diff --git a/src/lib/server/security-notify.ts b/src/lib/server/security-notify.ts new file mode 100644 index 0000000..1bc4fdc --- /dev/null +++ b/src/lib/server/security-notify.ts @@ -0,0 +1,54 @@ +import type { Locale } from "$lib/i18n/core"; +import { translate } from "$lib/i18n/server"; +import { sendSecurityAlertEmail } from "$lib/server/email"; + +// F2: 보안 알림 이벤트 종류. 각 kind 는 i18n `security_alert..{subject,heading,body}` 를 가진다. +export type SecurityEventKind = + | "password_changed" + | "password_reset_by_admin" + | "account_locked" + | "account_disabled" + | "mfa_enrolled" + | "mfa_disabled" + | "backup_codes_regenerated" + | "passkey_added" + | "passkey_removed"; + +// users.locale ("ko-KR"/"en-US"/"ja-JP" 등) → i18n Locale. en* 만 en, 그 외(미상 포함)는 ko 기본. +// 근거: 지원 Locale 은 ko|en 뿐이며(core.ts), 알림 미발송보다 ko 기본 발송이 안전. +export function toLocale(userLocale: string | null | undefined): Locale { + return (userLocale ?? "").toLowerCase().startsWith("en") ? "en" : "ko"; +} + +// UTC 기준 사람이 읽을 수 있는 타임스탬프(locale 무관, 시간대 모호성 제거). +function formatWhen(when: Date): string { + return when + .toISOString() + .replace("T", " ") + .replace(/\.\d+Z$/, " UTC"); +} + +/** + * 보안 알림 메일을 fire-and-forget 로 발송한다. 본 동작과 완전 격리한다: + * - 이메일이 없는 계정은 조용히 스킵 + * - 발송 실패는 무시(서버 로그만) + * - Workers 는 waitUntil 로 응답 경로에서 분리, Node 는 fire-and-forget + */ +export function dispatchSecurityAlert(params: { to: string | null | undefined; locale: string | null | undefined; kind: SecurityEventKind; when?: Date; platform?: App.Platform }): void { + const { to, kind, platform } = params; + if (!to) return; // 이메일 없는 계정 스킵 + const L = toLocale(params.locale); + const when = params.when ?? new Date(); + const content = { + subject: translate(L, `security_alert.${kind}.subject`), + heading: translate(L, `security_alert.${kind}.heading`), + body: translate(L, `security_alert.${kind}.body`), + whenText: translate(L, "security_alert.when", { time: formatWhen(when) }), + footer: translate(L, "security_alert.footer"), + }; + const sendPromise = sendSecurityAlertEmail(to, content, platform).catch((e) => { + console.error("[security-notify] 발송 실패", kind, e); + }); + const wait = platform?.ctx?.waitUntil?.bind(platform.ctx); + if (wait) wait(sendPromise); +} diff --git a/src/routes/(auth)/reset-password/+page.server.ts b/src/routes/(auth)/reset-password/+page.server.ts index 2e128fd..fecd6f3 100644 --- a/src/routes/(auth)/reset-password/+page.server.ts +++ b/src/routes/(auth)/reset-password/+page.server.ts @@ -13,6 +13,7 @@ import { resolveSkinHtml, replacePlaceholders, escapeHtml } from "$lib/server/sk import { checkRateLimit } from "$lib/server/ratelimit"; import { getRequestMetadata } from "$lib/server/audit"; import { translate } from "$lib/i18n/server"; +import { dispatchSecurityAlert } from "$lib/server/security-notify"; async function resolveSkin(skinHint: string | null, locals: App.Locals, platform: App.Platform | undefined, token: string | null, redirectTo: string | null, flashMsg = ""): Promise { if (!skinHint || !locals.db || !locals.tenant) return null; @@ -94,8 +95,9 @@ export const actions: Actions = { const now = new Date(); const [record] = await db - .select({ id: passwordResetTokens.id, userId: passwordResetTokens.userId, expiresAt: passwordResetTokens.expiresAt }) + .select({ id: passwordResetTokens.id, userId: passwordResetTokens.userId, expiresAt: passwordResetTokens.expiresAt, email: users.email, locale: users.locale }) .from(passwordResetTokens) + .innerJoin(users, eq(passwordResetTokens.userId, users.id)) .where(and(eq(passwordResetTokens.tokenHash, tokenHash), isNull(passwordResetTokens.usedAt))) .limit(1); @@ -131,6 +133,9 @@ export const actions: Actions = { await revokeAllUserSessions(db, record.userId, now); await revokeAllUserRefreshTokens(db, record.userId); + // 보안 알림(best-effort, 완전 격리) — 비밀번호가 변경됨. + dispatchSecurityAlert({ to: record.email, locale: record.locale, kind: "password_changed", when: now, platform: event.platform }); + const extra = new URLSearchParams(); if (redirectTo) extra.set("redirectTo", redirectTo); if (skinHint) extra.set("skinHint", skinHint); diff --git a/src/routes/(auth)/signup/+page.server.ts b/src/routes/(auth)/signup/+page.server.ts index ce1c8d2..2a708ae 100644 --- a/src/routes/(auth)/signup/+page.server.ts +++ b/src/routes/(auth)/signup/+page.server.ts @@ -10,6 +10,7 @@ import { sanitizeRedirectTarget } from "$lib/server/auth/redirect"; import { checkRateLimit } from "$lib/server/ratelimit"; import { getRequestMetadata } from "$lib/server/audit"; import { translate } from "$lib/i18n/server"; +import { issueEmailVerification } from "$lib/server/auth/email-verification"; export const load: PageServerLoad = async ({ locals, url, platform }) => { const skinHint = url.searchParams.get("skinHint"); @@ -109,6 +110,9 @@ export const actions: Actions = { await db.insert(credentials).values({ userId, type: "password", secret: hashedPw, label: "비밀번호", createdAt: now }); await db.insert(identities).values({ tenantId: tenant.id, userId, provider: "local", subject: email, email, linkedAt: now }); + // 이메일 인증 메일 발송 — 실패해도 가입은 성공 처리(격리). + await issueEmailVerification(db, userId, email, event.platform); + const redirectTo = sanitizeRedirectTarget(event.url.searchParams.get("redirectTo")); const skinHint = event.url.searchParams.get("skinHint") ?? ""; const extra = new URLSearchParams(); diff --git a/src/routes/(auth)/verify-email/+page.server.ts b/src/routes/(auth)/verify-email/+page.server.ts new file mode 100644 index 0000000..cf4e9e8 --- /dev/null +++ b/src/routes/(auth)/verify-email/+page.server.ts @@ -0,0 +1,81 @@ +import { fail } from "@sveltejs/kit"; +import { and, eq, isNull } from "drizzle-orm"; +import type { Actions, PageServerLoad } from "./$types"; +import { requireDbContext } from "$lib/server/auth/guards"; +import { users, emailVerificationTokens } from "$lib/server/db/schema"; +import { hashToken } from "$lib/server/email"; +import { runAtomic } from "$lib/server/db/atomic"; +import { checkRateLimit } from "$lib/server/ratelimit"; +import { recordAuditEvent, getRequestMetadata } from "$lib/server/audit"; +import { translate } from "$lib/i18n/server"; + +// 토큰 상태를 tenant 범위에서 조회한다(read-only). 유효하면 record 반환, 아니면 null. +async function lookupToken(db: App.Locals["db"], tenantId: string, token: string) { + if (!db) return null; + const tokenHash = await hashToken(token); + const now = new Date(); + const [record] = await db + .select({ tokenId: emailVerificationTokens.id, userId: users.id, expiresAt: emailVerificationTokens.expiresAt, emailVerifiedAt: users.emailVerifiedAt }) + .from(emailVerificationTokens) + .innerJoin(users, eq(emailVerificationTokens.userId, users.id)) + .where(and(eq(emailVerificationTokens.tokenHash, tokenHash), isNull(emailVerificationTokens.usedAt), eq(users.tenantId, tenantId))) + .limit(1); + if (!record || record.expiresAt < now) return null; + return record; +} + +export const load: PageServerLoad = async ({ locals, url }) => { + const token = url.searchParams.get("token"); + if (!token || !locals.db || !locals.tenant) { + return { valid: false, token: null as string | null, alreadyVerified: false }; + } + const record = await lookupToken(locals.db, locals.tenant.id, token); + if (!record) return { valid: false, token: null as string | null, alreadyVerified: false }; + // 이미 인증된 계정의 유효 토큰이면 성공 화면으로 바로 안내(멱등). + return { valid: true, token: token as string | null, alreadyVerified: !!record.emailVerifiedAt }; +}; + +export const actions: Actions = { + default: async (event) => { + const { db, tenant } = requireDbContext(event.locals); + const locale = event.locals.locale; + + const formData = await event.request.formData(); + const token = String(formData.get("token") ?? ""); + + // 토큰 제출 브루트포스 방어 — 형제 인증 라우트와 동일하게 IP 당 제한. + const meta = getRequestMetadata(event); + const rl = await checkRateLimit(db, `verify-email:${meta.ipKey}`, { windowMs: 15 * 60 * 1000, limit: 10 }); + if (!rl.allowed) { + return fail(429, { error: translate(locale, "errors.rate_limit", { minutes: Math.ceil(rl.retryAfterMs / 60000) }) }); + } + + if (!token) return fail(400, { error: translate(locale, "verify_email.invalid_link") }); + + const record = await lookupToken(db, tenant.id, token); + if (!record) return fail(400, { error: translate(locale, "verify_email.invalid_link") }); + + const now = new Date(); + // 1회용 소진 + emailVerifiedAt 세팅을 원자적으로. 같은 user 의 미사용 토큰을 모두 소진해 재사용 차단. + await runAtomic(db, [ + (h) => + h + .update(emailVerificationTokens) + .set({ usedAt: now }) + .where(and(eq(emailVerificationTokens.userId, record.userId), isNull(emailVerificationTokens.usedAt))), + (h) => h.update(users).set({ emailVerifiedAt: now, updatedAt: now }).where(eq(users.id, record.userId)), + ]); + + await recordAuditEvent(db, { + tenantId: tenant.id, + userId: record.userId, + actorId: record.userId, + kind: "email_verified", + outcome: "success", + ip: meta.ip, + userAgent: meta.userAgent, + }); + + return { verified: true }; + }, +}; diff --git a/src/routes/(auth)/verify-email/+page.svelte b/src/routes/(auth)/verify-email/+page.svelte new file mode 100644 index 0000000..55920bb --- /dev/null +++ b/src/routes/(auth)/verify-email/+page.svelte @@ -0,0 +1,50 @@ + + +
+
+
+
+

{t("verify_email.title")}

+

{t("verify_email.subtitle")}

+
+ + {#if isSuccess} +
+ {t("verify_email.success")} +
+ + + {t("verify_email.go_login")} → + + {:else if data.valid} + {#if err} +
{err}
+ {/if} +

{t("verify_email.confirm_prompt")}

+
+ + +
+ {:else} +
+ {t("verify_email.invalid_link")} +
+ + + {t("verify_email.go_login")} → + + {/if} +
+
diff --git a/src/routes/account/mfa/+page.server.ts b/src/routes/account/mfa/+page.server.ts index 7615839..90776b3 100644 --- a/src/routes/account/mfa/+page.server.ts +++ b/src/routes/account/mfa/+page.server.ts @@ -7,6 +7,7 @@ import { generateTotpSecret, buildOtpAuthUri, verifyTotp, encryptTotpSecret, dec import { TOTP_CREDENTIAL_TYPE, BACKUP_CODE_CREDENTIAL_TYPE } from "$lib/server/auth/constants"; import { credentials } from "$lib/server/db/schema"; import { recordAuditEvent, getRequestMetadata } from "$lib/server/audit"; +import { dispatchSecurityAlert } from "$lib/server/security-notify"; const TOTP_SETUP_COOKIE = "idp_totp_setup"; const TOTP_SETUP_TTL_MS = 10 * 60 * 1000; // 10분 @@ -224,6 +225,8 @@ export const actions: Actions = { userAgent: requestMetadata.userAgent, }); + dispatchSecurityAlert({ to: locals.user.email, locale: locals.user.locale, kind: "mfa_enrolled", platform: event.platform }); + return { confirm: true, backupCodes: codes }; }, @@ -279,6 +282,8 @@ export const actions: Actions = { userAgent: requestMetadata.userAgent, }); + dispatchSecurityAlert({ to: locals.user.email, locale: locals.user.locale, kind: "mfa_disabled", platform: event.platform }); + return { deleted: true }; }, @@ -347,6 +352,8 @@ export const actions: Actions = { userAgent: requestMetadata.userAgent, }); + dispatchSecurityAlert({ to: locals.user.email, locale: locals.user.locale, kind: "backup_codes_regenerated", platform: event.platform }); + return { regenerate: true, backupCodes: codes }; }, }; diff --git a/src/routes/account/passkeys/+page.server.ts b/src/routes/account/passkeys/+page.server.ts index 2a38c7d..20dd761 100644 --- a/src/routes/account/passkeys/+page.server.ts +++ b/src/routes/account/passkeys/+page.server.ts @@ -3,6 +3,7 @@ import { eq, and } from "drizzle-orm"; import type { Actions, PageServerLoad } from "./$types"; import { requireDbContext } from "$lib/server/auth/guards"; import { recordAuditEvent, getRequestMetadata } from "$lib/server/audit/index"; +import { dispatchSecurityAlert } from "$lib/server/security-notify"; import { credentials } from "$lib/server/db/schema"; import { WEBAUTHN_CREDENTIAL_TYPE } from "$lib/server/auth/constants"; import { findPasswordCredential } from "$lib/server/auth/users"; @@ -90,6 +91,8 @@ export const actions: Actions = { userAgent: requestMetadata.userAgent, }); + dispatchSecurityAlert({ to: locals.user.email, locale: locals.user.locale, kind: "passkey_removed", platform: event.platform }); + return { deleted: true }; }, }; diff --git a/src/routes/account/profile/+page.server.ts b/src/routes/account/profile/+page.server.ts index 1d137ef..c071784 100644 --- a/src/routes/account/profile/+page.server.ts +++ b/src/routes/account/profile/+page.server.ts @@ -4,6 +4,9 @@ import type { Actions, PageServerLoad } from "./$types"; import { requireDbContext } from "$lib/server/auth/guards"; import { users } from "$lib/server/db/schema"; import { getUserMembership } from "$lib/server/org/membership"; +import { issueEmailVerification } from "$lib/server/auth/email-verification"; +import { checkRateLimit } from "$lib/server/ratelimit"; +import { translate } from "$lib/i18n/server"; export const load: PageServerLoad = async ({ locals }) => { if (!locals.user) throw redirect(303, "/login"); @@ -23,12 +26,14 @@ export const load: PageServerLoad = async ({ locals }) => { bio: locals.user.bio, birthdate: locals.user.birthdate, }, + email: locals.user.email, + emailVerified: !!locals.user.emailVerifiedAt, membership, }; }; export const actions: Actions = { - default: async ({ locals, request }) => { + save: async ({ locals, request }) => { if (!locals.user) throw redirect(303, "/login"); const { db, tenant } = requireDbContext(locals); @@ -64,4 +69,25 @@ export const actions: Actions = { return { success: true }; }, + + // 이메일 인증 메일 재발송. 이미 인증됐으면 no-op. rate-limit(기존 인프라 재사용). + resendVerification: async (event) => { + const { locals } = event; + if (!locals.user) throw redirect(303, "/login"); + const { db } = requireDbContext(locals); + const locale = locals.locale; + + // 이미 인증된 계정은 조용히 no-op(성공 응답). + if (locals.user.emailVerifiedAt) { + return { resent: true }; + } + + const rl = await checkRateLimit(db, `resend-verification:${locals.user.id}`, { windowMs: 60 * 60 * 1000, limit: 5 }); + if (!rl.allowed) { + return fail(429, { resendError: translate(locale, "errors.rate_limit", { minutes: Math.ceil(rl.retryAfterMs / 60000) }) }); + } + + await issueEmailVerification(db, locals.user.id, locals.user.email, event.platform); + return { resent: true }; + }, }; diff --git a/src/routes/account/profile/+page.svelte b/src/routes/account/profile/+page.svelte index 3bbf653..2f41c40 100644 --- a/src/routes/account/profile/+page.svelte +++ b/src/routes/account/profile/+page.svelte @@ -7,6 +7,8 @@ const { data, form } = $props<{ data: PageData; form?: ActionData }>(); const success = $derived((form as { success?: boolean } | null)?.success ?? false); const err = $derived((form as { error?: string } | null)?.error ?? null); +const resent = $derived((form as { resent?: boolean } | null)?.resent ?? false); +const resendError = $derived((form as { resendError?: string } | null)?.resendError ?? null); const LOCALE_OPTIONS = [ { value: "ko-KR", label: "한국어" }, @@ -34,7 +36,29 @@ const TIMEZONE_OPTIONS = [ {/if} -
+ {#if !data.emailVerified} +
+
+
+

{t("profile.email_unverified_title")}

+

{t("profile.email_unverified_desc", { email: data.email })}

+
+ + + +
+ {#if resent} +

{t("profile.email_resend_sent")}

+ {/if} + {#if resendError} +

{resendError}

+ {/if} +
+ {/if} + +

{t("profile.basic_info")}

diff --git a/src/routes/api/webauthn/register/verify/+server.ts b/src/routes/api/webauthn/register/verify/+server.ts index fc33c38..6db57e3 100644 --- a/src/routes/api/webauthn/register/verify/+server.ts +++ b/src/routes/api/webauthn/register/verify/+server.ts @@ -3,6 +3,7 @@ import type { RequestHandler } from "./$types"; import { requireDbContext } from "$lib/server/auth/guards"; import { getRuntimeConfig } from "$lib/server/auth/runtime"; import { recordAuditEvent, getRequestMetadata } from "$lib/server/audit/index"; +import { dispatchSecurityAlert } from "$lib/server/security-notify"; import { checkRateLimit } from "$lib/server/ratelimit"; import { verifyChallengeCookie, verifyRegistrationResponse, savePasskey, getWebAuthnConfig, WEBAUTHN_CHALLENGE_COOKIE } from "$lib/server/auth/webauthn"; import type { RegistrationResponseJSON } from "$lib/server/auth/webauthn"; @@ -94,5 +95,7 @@ export const POST: RequestHandler = async (event) => { userAgent: requestMetadata.userAgent, }); + dispatchSecurityAlert({ to: locals.user.email, locale: locals.user.locale, kind: "passkey_added", platform }); + return json({ ok: true }); }; From d48992c3dd491706b9795d1e2ae3de3d234eadcf Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 11:14:35 +0900 Subject: [PATCH 06/26] =?UTF-8?q?test:=20P0=20=EB=B3=B4=EC=95=88=20?= =?UTF-8?q?=ED=95=B5=EC=8B=AC=20=EB=A1=9C=EC=A7=81=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=2089=EA=B0=9C=20=EC=B6=94=EA=B0=80=20+=20parity=20?= =?UTF-8?q?=EA=B0=95=ED=99=94=20+=20=EC=BB=A4=EB=B2=84=EB=A6=AC=EC=A7=80?= =?UTF-8?q?=20=EC=9D=B8=ED=94=84=EB=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 신규 8파일: crypto-keys(22)·password(19)·oidc-grant(6)·oidc-refresh(8)·ratelimit(7)·email-verification(6)·session(9)·gc(7) — WebCrypto 독립 교차검증, mock DB 실계약 준수(총 151 테스트) - schema-parity 강화: nullable·타입 계열·인덱스/unique 비교, 방언별 정당한 차이는 명시적 예외 목록(signing_keys mysql partial-unique 1건) - @vitest/coverage-v8 + test:coverage / typecheck(tsc) / lint:fix / db:check(4방언) 스크립트 - lefthook pre-commit에 eslint 추가(svelte config 직렬화 이슈로 --cache 미사용) Co-Authored-By: Claude Fable 5 --- .gitignore | 6 + bun.lock | 39 +++- lefthook.yml | 3 + package.json | 14 +- test/unit/crypto-keys.test.ts | 260 +++++++++++++++++++++++++++ test/unit/email-verification.test.ts | 108 +++++++++++ test/unit/gc.test.ts | 186 +++++++++++++++++++ test/unit/oidc-grant.test.ts | 124 +++++++++++++ test/unit/oidc-refresh.test.ts | 218 ++++++++++++++++++++++ test/unit/password.test.ts | 158 ++++++++++++++++ test/unit/ratelimit.test.ts | 141 +++++++++++++++ test/unit/schema-parity.test.ts | 252 +++++++++++++++++++++++--- test/unit/session.test.ts | 225 +++++++++++++++++++++++ vitest.config.ts | 7 + 14 files changed, 1713 insertions(+), 28 deletions(-) create mode 100644 test/unit/crypto-keys.test.ts create mode 100644 test/unit/email-verification.test.ts create mode 100644 test/unit/gc.test.ts create mode 100644 test/unit/oidc-grant.test.ts create mode 100644 test/unit/oidc-refresh.test.ts create mode 100644 test/unit/password.test.ts create mode 100644 test/unit/ratelimit.test.ts create mode 100644 test/unit/session.test.ts diff --git a/.gitignore b/.gitignore index f316eca..490df1d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,12 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# Test coverage +/coverage + +# ESLint cache +.eslintcache # SQLite *.db diff --git a/bun.lock b/bun.lock index 795d1ad..7e917f5 100644 --- a/bun.lock +++ b/bun.lock @@ -33,6 +33,7 @@ "@types/node": "^26.1.0", "@types/nodemailer": "^8.0.0", "@types/qrcode": "^1.5.6", + "@vitest/coverage-v8": "^4.1.9", "drizzle-kit": "^0.31.10", "drizzle-orm": "^0.45.2", "eslint": "^10.3.0", @@ -59,6 +60,16 @@ "ws": "^8.21.0", }, "packages": { + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], @@ -503,6 +514,8 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg=="], + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.9", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.9", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.9", "vitest": "4.1.9" }, "optionalPeers": ["@vitest/browser"] }, "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g=="], + "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], "@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], @@ -543,6 +556,8 @@ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA=="], + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], @@ -685,8 +700,12 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -709,10 +728,18 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -787,6 +814,10 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + "miniflare": ["miniflare@4.20260630.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260630.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-lyRplDrSJJWVpzSSQPBSQtNmUuxScCZyOOkXFs37uSbdTfWRDDmw6DyFKVS2s1eYtA/i4u2xR/0FyPIsTl/HJw=="], "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], @@ -925,7 +956,7 @@ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], @@ -1037,6 +1068,8 @@ "@ldapjs/controls/@ldapjs/asn1": ["@ldapjs/asn1@1.2.0", "", {}, "sha512-KX/qQJ2xxzvO2/WOvr1UdQ+8P5dVvuOLk/C9b1bIkXxZss8BaR28njXdPgFCpj5aHaf1t8PmuVnea+N9YG9YMw=="], + "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + "@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], "@simplewebauthn/server/@peculiar/x509": ["@peculiar/x509@1.14.3", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA=="], @@ -1079,6 +1112,8 @@ "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "ast-v8-to-istanbul/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "eslint-plugin-svelte/globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], @@ -1157,6 +1192,8 @@ "@vitest/mocker/estree-walker/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "ast-v8-to-istanbul/estree-walker/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], diff --git a/lefthook.yml b/lefthook.yml index 337a7b6..e520534 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -5,3 +5,6 @@ pre-commit: glob: "*.{ts,tsx,js,mjs,cjs,svelte,html,css,json,jsonc,md,yml,yaml}" run: bunx prettier --write {staged_files} stage_fixed: true + - name: eslint + glob: "*.{ts,tsx,js,mjs,cjs,svelte}" + run: bunx eslint --no-warn-ignored {staged_files} diff --git a/package.json b/package.json index eca14a0..3faa3dc 100644 --- a/package.json +++ b/package.json @@ -10,15 +10,23 @@ "prepare": "svelte-kit sync || echo '' && lefthook install || echo ''", "check": "wrangler types --check && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "typecheck": "tsc --noEmit", "lint": "prettier --check . && eslint .", + "lint:fix": "eslint --fix . && prettier --write .", "format": "prettier --write .", "gen": "wrangler types", "deploy": "wrangler deploy", "deploy:build": "vite build && wrangler deploy", - "db:generate": "drizzle-kit generate", + "db:generate": "drizzle-kit generate --dialect=sqlite --schema=./src/lib/server/db/schema.sqlite.ts --out=./drizzle", "db:generate:pg": "DB_DIALECT=postgres drizzle-kit generate", "db:generate:mysql": "DB_DIALECT=mysql drizzle-kit generate", "db:generate:sqlite": "DB_DIALECT=sqlite drizzle-kit generate", + "db:generate:all": "bun run db:generate && bun run db:generate:pg && bun run db:generate:mysql && bun run db:generate:sqlite", + "db:check": "bun run db:check:d1 && bun run db:check:pg && bun run db:check:mysql && bun run db:check:sqlite", + "db:check:d1": "drizzle-kit check", + "db:check:pg": "DB_DIALECT=postgres drizzle-kit check", + "db:check:mysql": "DB_DIALECT=mysql drizzle-kit check", + "db:check:sqlite": "DB_DIALECT=sqlite drizzle-kit check", "db:push": "drizzle-kit push", "db:migrate": "drizzle-kit migrate && bun run db:seed:migrate", "db:migrate:preview": "CLOUDFLARE_IS_PREVIEW=true drizzle-kit migrate && bun run db:seed:migrate:preview", @@ -46,7 +54,8 @@ "setup": "bun run scripts/setup.ts", "verify:saml-encryption": "bun scripts/verify-saml-encryption.ts", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "devDependencies": { "@eslint/compat": "^2.1.0", @@ -60,6 +69,7 @@ "@types/node": "^26.1.0", "@types/nodemailer": "^8.0.0", "@types/qrcode": "^1.5.6", + "@vitest/coverage-v8": "^4.1.9", "drizzle-kit": "^0.31.10", "drizzle-orm": "^0.45.2", "eslint": "^10.3.0", diff --git a/test/unit/crypto-keys.test.ts b/test/unit/crypto-keys.test.ts new file mode 100644 index 0000000..3c94000 --- /dev/null +++ b/test/unit/crypto-keys.test.ts @@ -0,0 +1,260 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import type { DB } from "$lib/server/db"; +import { + b64uEncode, + b64uDecode, + generateRsaSigningKey, + signJwt, + verifyIdToken, + generateAccessToken, + verifyAccessToken, + wrapPrivateKey, + unwrapPrivateKey, + encryptSecret, + decryptSecret, + type AccessTokenClaims, +} from "$lib/server/crypto/keys"; + +// ── 독립 교차 검증 헬퍼 ──────────────────────────────────────────────────────── +// keys.ts 의 b64u 구현에 의존하지 않는 별도 base64url 디코더 (tautology 회피용). +function b64urlToBytes(s: string): Uint8Array { + const b64 = s.replace(/-/g, "+").replace(/_/g, "/"); + const pad = b64.length % 4 ? "=".repeat(4 - (b64.length % 4)) : ""; + const bin = atob(b64 + pad); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); + return arr; +} +function b64urlToString(s: string): string { + return new TextDecoder().decode(b64urlToBytes(s)); +} + +const nowSec = () => Math.floor(Date.now() / 1000); + +// verifyIdToken 은 db 파라미터로 공개 JWK 행을 조회한다. 실제 암호 검증 로직 +// (서명/typ/crit/events/exp/aud/iss) 을 그대로 태우기 위한 최소 쿼리빌더 스텁. +function fakeDb(publicJwk: JsonWebKey | null): DB { + const rows = publicJwk ? [{ publicJwk: JSON.stringify(publicJwk) }] : []; + return { + select: () => ({ from: () => ({ where: () => ({ limit: async () => rows }) }) }), + } as unknown as DB; +} + +describe("b64u 헬퍼", () => { + it("인코딩→디코딩 라운드트립 + 독립 디코더와 교차 일치", () => { + const bytes = new Uint8Array([0, 1, 2, 250, 251, 252, 253, 254, 255, 42, 7]); + const encoded = b64uEncode(bytes); + // padding 없음 + url-safe 문자만 + expect(encoded).not.toMatch(/[+/=]/); + expect(Array.from(b64uDecode(encoded))).toEqual(Array.from(bytes)); + // 독립 디코더로도 동일 결과 + expect(Array.from(b64urlToBytes(encoded))).toEqual(Array.from(bytes)); + }); +}); + +describe("RS256 JWT (signJwt / verifyIdToken)", () => { + let key: Awaited>; + + beforeAll(async () => { + key = await generateRsaSigningKey(); + }); + + it("signJwt 결과를 WebCrypto 로 직접 검증 (독립 교차검증)", async () => { + const payload = { sub: "user-1", aud: "client-a", iss: "https://idp.example", exp: nowSec() + 3600, iat: nowSec() }; + const token = await signJwt(payload, key.privateKey, key.kid); + const [h, p, s] = token.split("."); + + // 헤더/페이로드를 독립 디코더로 파싱 + const header = JSON.parse(b64urlToString(h)) as Record; + expect(header.alg).toBe("RS256"); + expect(header.typ).toBe("JWT"); + expect(header.kid).toBe(key.kid); + expect(JSON.parse(b64urlToString(p))).toEqual(payload); + + // jose 없이 공개 JWK 를 import 해 서명을 직접 검증 + const pub = await crypto.subtle.importKey("jwk", key.publicJwk, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["verify"]); + const ok = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", pub, b64urlToBytes(s), new TextEncoder().encode(`${h}.${p}`)); + expect(ok).toBe(true); + }); + + it("custom typ 헤더가 반영된다", async () => { + const token = await signJwt({ sub: "x" }, key.privateKey, key.kid, { typ: "logout+jwt" }); + const header = JSON.parse(b64urlToString(token.split(".")[0])) as Record; + expect(header.typ).toBe("logout+jwt"); + }); + + it("유효 토큰: verifyIdToken 이 claims 반환 (aud/iss 일치)", async () => { + const payload = { sub: "user-1", aud: "client-a", iss: "https://idp.example", exp: nowSec() + 3600 }; + const token = await signJwt(payload, key.privateKey, key.kid); + const claims = await verifyIdToken(fakeDb(key.publicJwk), "tenant-1", token, { expectedAud: "client-a", expectedIssuer: "https://idp.example" }); + expect(claims).not.toBeNull(); + expect(claims!.sub).toBe("user-1"); + }); + + it("변조된 서명 거부", async () => { + const token = await signJwt({ sub: "u", aud: "client-a", exp: nowSec() + 3600 }, key.privateKey, key.kid); + const [h, p, s] = token.split("."); + // 서명 첫 문자 치환 — 마지막 base64url 문자는 미사용(패딩) 비트만 담을 수 있어 + // 같은 바이트로 디코드될 수 있으므로 유효 비트가 확실한 첫 문자를 바꾼다. + const tamperedChar = s[0] === "A" ? "B" : "A"; + const tampered = `${h}.${p}.${tamperedChar}${s.slice(1)}`; + expect(await verifyIdToken(fakeDb(key.publicJwk), "tenant-1", tampered)).toBeNull(); + }); + + it("잘못된 aud 거부", async () => { + const token = await signJwt({ sub: "u", aud: "client-a", exp: nowSec() + 3600 }, key.privateKey, key.kid); + expect(await verifyIdToken(fakeDb(key.publicJwk), "tenant-1", token, { expectedAud: "client-b" })).toBeNull(); + }); + + it("만료 토큰 거부 (ignoreExpiry 로는 통과)", async () => { + const token = await signJwt({ sub: "u", aud: "client-a", exp: nowSec() - 10 }, key.privateKey, key.kid); + expect(await verifyIdToken(fakeDb(key.publicJwk), "tenant-1", token, { expectedAud: "client-a" })).toBeNull(); + // 만료 무시 옵션에서는 서명 유효 → 통과 + const claims = await verifyIdToken(fakeDb(key.publicJwk), "tenant-1", token, { expectedAud: "client-a", ignoreExpiry: true }); + expect(claims).not.toBeNull(); + }); + + it("type-confusion 방어: 비-JWT typ / events claim 거부", async () => { + const logoutTyped = await signJwt({ sub: "u", exp: nowSec() + 3600 }, key.privateKey, key.kid, { typ: "logout+jwt" }); + expect(await verifyIdToken(fakeDb(key.publicJwk), "tenant-1", logoutTyped)).toBeNull(); + + const withEvents = await signJwt({ sub: "u", exp: nowSec() + 3600, events: { "http://schemas.openid.net/event/backchannel-logout": {} } }, key.privateKey, key.kid); + expect(await verifyIdToken(fakeDb(key.publicJwk), "tenant-1", withEvents)).toBeNull(); + }); + + it("kid 미매칭(행 없음) 거부", async () => { + const token = await signJwt({ sub: "u", exp: nowSec() + 3600 }, key.privateKey, key.kid); + expect(await verifyIdToken(fakeDb(null), "tenant-1", token)).toBeNull(); + }); + + it("잘못된 형식(파트 수 불일치) 거부", async () => { + expect(await verifyIdToken(fakeDb(key.publicJwk), "tenant-1", "not.a.jwt.token")).toBeNull(); + expect(await verifyIdToken(fakeDb(key.publicJwk), "tenant-1", "onlyone")).toBeNull(); + }); +}); + +describe("HMAC opaque access token (generate/verifyAccessToken)", () => { + const secret = "hmac-secret-abcdefghijklmnop-1234567890"; + const baseClaims = (over: Partial = {}): AccessTokenClaims => ({ + sub: "user-1", + tenantId: "tenant-1", + clientId: "client-a", + scope: "openid profile", + exp: nowSec() + 3600, + iat: nowSec(), + aud: "client-a", + iss: "https://idp.example", + ...over, + }); + + it("발급 → 검증 라운드트립", async () => { + const claims = baseClaims(); + const token = await generateAccessToken(claims, secret); + const verified = await verifyAccessToken(token, secret, "tenant-1", "client-a"); + expect(verified).toEqual(claims); + }); + + it("HMAC 서명을 WebCrypto 로 직접 검증 (독립 교차검증)", async () => { + const token = await generateAccessToken(baseClaims(), secret); + const [data, sig] = token.split("."); + const macKey = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]); + const ok = await crypto.subtle.verify("HMAC", macKey, b64urlToBytes(sig), new TextEncoder().encode(data)); + expect(ok).toBe(true); + }); + + it("페이로드 변조 거부 (서명 미갱신)", async () => { + const token = await generateAccessToken(baseClaims(), secret); + const [, sig] = token.split("."); + const forged = { ...baseClaims(), sub: "attacker" }; + const forgedData = b64uEncode(new TextEncoder().encode(JSON.stringify(forged))); + expect(await verifyAccessToken(`${forgedData}.${sig}`, secret, "tenant-1")).toBeNull(); + }); + + it("서명 변조 거부", async () => { + const token = await generateAccessToken(baseClaims(), secret); + const [data, sig] = token.split("."); + // 첫 문자 치환 (마지막 문자는 미사용 비트만 담아 같은 바이트로 디코드될 수 있음). + const ch = sig[0] === "A" ? "B" : "A"; + expect(await verifyAccessToken(`${data}.${ch}${sig.slice(1)}`, secret, "tenant-1")).toBeNull(); + }); + + it("만료 거부", async () => { + const token = await generateAccessToken(baseClaims({ exp: nowSec() - 10 }), secret); + expect(await verifyAccessToken(token, secret, "tenant-1")).toBeNull(); + }); + + it("tenant 불일치 거부", async () => { + const token = await generateAccessToken(baseClaims(), secret); + expect(await verifyAccessToken(token, secret, "tenant-OTHER")).toBeNull(); + }); + + it("aud 불일치 거부", async () => { + const token = await generateAccessToken(baseClaims(), secret); + expect(await verifyAccessToken(token, secret, "tenant-1", "client-B")).toBeNull(); + }); + + it("잘못된 secret 거부", async () => { + const token = await generateAccessToken(baseClaims(), secret); + expect(await verifyAccessToken(token, "wrong-secret", "tenant-1")).toBeNull(); + }); +}); + +describe("private key 래핑 (wrapPrivateKey / unwrapPrivateKey)", () => { + let key: Awaited>; + const secret = "wrap-secret-0123456789"; + + beforeAll(async () => { + key = await generateRsaSigningKey(); + }); + + it("래핑 → 언래핑 라운드트립 (복원 키로 서명한 값이 원본 공개키로 검증됨)", async () => { + const wrapped = await wrapPrivateKey(key.privateKey, secret); + expect(wrapped.split(".")).toHaveLength(3); // salt.iv.ct + const unwrapped = await unwrapPrivateKey(wrapped, secret); + const msg = new TextEncoder().encode("payload-to-sign"); + const sig = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", unwrapped, msg); + const ok = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, sig, msg); + expect(ok).toBe(true); + }); + + it("잘못된 secret 으로 언래핑 실패 (AES-GCM 인증 태그)", async () => { + const wrapped = await wrapPrivateKey(key.privateKey, secret); + await expect(unwrapPrivateKey(wrapped, "wrong-secret")).rejects.toThrow(); + }); + + it("잘못된 형식 거부", async () => { + await expect(unwrapPrivateKey("only.two", secret)).rejects.toThrow(/Invalid encrypted key format/); + }); +}); + +describe("secret 암호화 + HKDF 도메인 분리 (encryptSecret / decryptSecret)", () => { + const secret = "master-secret-abcdef-0123456789"; + + it("기본 context 라운드트립", async () => { + const enc = await encryptSecret("hello world", secret); + expect(enc.split(".")).toHaveLength(3); + expect(await decryptSecret(enc, secret)).toBe("hello world"); + }); + + it("커스텀 context 라운드트립", async () => { + const enc = await encryptSecret("payload", secret, "context-A"); + expect(await decryptSecret(enc, secret, "context-A")).toBe("payload"); + }); + + it("도메인 분리: 다른 context 로는 복호화 불가", async () => { + const enc = await encryptSecret("payload", secret, "context-A"); + // 같은 secret 이라도 HKDF info(context) 가 다르면 파생 키가 달라 복호 실패 + await expect(decryptSecret(enc, secret, "context-B")).rejects.toThrow(); + // 기본 context 로도 실패 (context-A 로 암호화했으므로) + await expect(decryptSecret(enc, secret)).rejects.toThrow(); + }); + + it("잘못된 masterSecret 으로 복호화 실패", async () => { + const enc = await encryptSecret("payload", secret, "context-A"); + await expect(decryptSecret(enc, "wrong-master", "context-A")).rejects.toThrow(); + }); + + it("잘못된 형식 거부", async () => { + await expect(decryptSecret("only.two", secret)).rejects.toThrow(/Invalid encrypted secret format/); + }); +}); diff --git a/test/unit/email-verification.test.ts b/test/unit/email-verification.test.ts new file mode 100644 index 0000000..e90e7fe --- /dev/null +++ b/test/unit/email-verification.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { env } from "$env/dynamic/private"; +import type { DB } from "$lib/server/db"; +import { generateToken, hashToken } from "$lib/server/email"; +import { issueEmailVerification, EMAIL_VERIFY_EXPIRY_MS } from "$lib/server/auth/email-verification"; + +// ── mock 설계 요약 ──────────────────────────────────────────────────────────── +// 대상 계약: +// generateToken() : CSPRNG 32바이트 → hex 토큰 + SHA-256 hex 해시. +// issueEmailVerification() : db.insert(emailVerificationTokens).values({userId,tokenHash,expiresAt}) +// (평문 토큰 미저장). 발송 실패/issuer 미설정을 격리. +// env 는 vitest 스텁($env/dynamic/private, 빈 가변 객체)을 공유하므로 테스트에서 +// IDP_ISSUER_URL 를 주입/제거해 분기를 제어한다. SMTP_* 미설정 → 실제 발송은 throw +// 하지만 issueEmailVerification 내부 .catch/try 로 격리되어 상위로 새지 않아야 한다. +function makeDb(onInsert?: () => void) { + const inserts: Record[] = []; + const db = { + insert: () => ({ + values: async (v: Record) => { + if (onInsert) onInsert(); + inserts.push(v); + }, + }), + }; + return { db: db as unknown as DB, inserts }; +} + +const HEX64 = /^[0-9a-f]{64}$/; + +// env($env/dynamic/private)는 선언상 필수 string 속성이라 delete가 막힌다. +// 동일 객체를 optional 뷰로만 캐스팅해(런타임 동작 동일) 키 제거를 허용한다. +const mutEnv = env as Record; + +beforeEach(() => { + // 발송 경로가 실제 네트워크를 타지 않도록 SMTP 미설정 유지(발송은 throw→격리 검증). + delete mutEnv.IDP_ISSUER_URL; + delete mutEnv.SMTP_HOSTNAME; + delete mutEnv.SMTP_PORTNUMB; + delete mutEnv.SMTP_USERNAME; + delete mutEnv.SMTP_PASSWORD; + vi.spyOn(console, "error").mockImplementation(() => {}); +}); +afterEach(() => { + vi.restoreAllMocks(); + delete mutEnv.IDP_ISSUER_URL; +}); + +describe("generateToken — CSPRNG 32바이트 / SHA-256 해시", () => { + it("token 은 64 hex(32바이트) URL-safe, tokenHash 는 token 의 SHA-256 hex", async () => { + const { token, tokenHash } = await generateToken(); + expect(HEX64.test(token)).toBe(true); // hex → URL-safe + expect(HEX64.test(tokenHash)).toBe(true); + expect(tokenHash).not.toBe(token); + // 해시 계약: tokenHash === SHA-256(token) + expect(tokenHash).toBe(await hashToken(token)); + }); + + it("호출마다 다른 토큰(CSPRNG 무작위성)", async () => { + const a = await generateToken(); + const b = await generateToken(); + expect(a.token).not.toBe(b.token); + expect(a.tokenHash).not.toBe(b.tokenHash); + }); +}); + +describe("issueEmailVerification — 토큰 저장/TTL/격리", () => { + it("issuer 설정 시 tokenHash(SHA-256)만 insert, 평문 토큰은 저장 안 함", async () => { + env.IDP_ISSUER_URL = "https://idp.example.com"; + const { db, inserts } = makeDb(); + const before = Date.now(); + await issueEmailVerification(db, "user-1", "u@example.com", undefined); + + expect(inserts.length).toBe(1); + const v = inserts[0]; + expect(v.userId).toBe("user-1"); + expect(HEX64.test(v.tokenHash as string)).toBe(true); // 해시만 저장 + expect(v.token).toBeUndefined(); // 평문 토큰 컬럼 없음 + // TTL 24h. + const exp = (v.expiresAt as Date).getTime(); + expect(exp).toBeGreaterThanOrEqual(before + EMAIL_VERIFY_EXPIRY_MS - 2000); + expect(exp).toBeLessThanOrEqual(Date.now() + EMAIL_VERIFY_EXPIRY_MS + 2000); + expect(EMAIL_VERIFY_EXPIRY_MS).toBe(24 * 60 * 60 * 1000); + }); + + it("발송 실패(SMTP 미설정 → send throw)가 상위로 새지 않고 격리된다", async () => { + env.IDP_ISSUER_URL = "https://idp.example.com"; + // SMTP_* 미설정이므로 sendEmailVerificationEmail 내부에서 throw 하지만 + // .catch/try 로 삼켜야 한다 — resolve 되어야 한다(토큰 insert 는 성공). + const { db, inserts } = makeDb(); + await expect(issueEmailVerification(db, "user-1", "u@example.com", undefined)).resolves.toBeUndefined(); + expect(inserts.length).toBe(1); + }); + + it("IDP_ISSUER_URL 미설정 시 발송/저장 스킵(insert 없음)", async () => { + // beforeEach 에서 IDP_ISSUER_URL 삭제됨. + const { db, inserts } = makeDb(); + await expect(issueEmailVerification(db, "user-1", "u@example.com", undefined)).resolves.toBeUndefined(); + expect(inserts.length).toBe(0); + }); + + it("토큰 발급/insert 예외도 격리되어 상위로 전파되지 않는다", async () => { + env.IDP_ISSUER_URL = "https://idp.example.com"; + const db = makeDb(() => { + throw new Error("db down"); + }).db; + await expect(issueEmailVerification(db, "user-1", "u@example.com", undefined)).resolves.toBeUndefined(); + }); +}); diff --git a/test/unit/gc.test.ts b/test/unit/gc.test.ts new file mode 100644 index 0000000..c7ac3c9 --- /dev/null +++ b/test/unit/gc.test.ts @@ -0,0 +1,186 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { getTableName, type SQL } from "drizzle-orm"; +import { SQLiteSyncDialect } from "drizzle-orm/sqlite-core"; +import type { DB } from "$lib/server/db"; +import { runExpiredDataGc } from "$lib/server/db/gc"; +import { REFRESH_TOKEN_TTL_MS } from "$lib/server/oidc/refresh"; +import { SESSION_TTL_MS } from "$lib/server/auth/constants"; + +// ── 순수 로직 검증용 목(mock) ────────────────────────────────────────────── +// runExpiredDataGc 는 각 테이블에 대해 db.delete(table).where(cond) 만 호출한다. +// (직접 DELETE + import 된 purge 함수 모두 동일 shape.) mock 은 delete 대상 테이블명과 +// 전달된 where SQL 을 캡처하고, 선택적으로 특정 테이블에서 예외를 던져 에러 격리를 검증한다. +const dialect = new SQLiteSyncDialect(); +function render(where: SQL): { sql: string; params: unknown[] } { + const q = dialect.sqlToQuery(where); + return { sql: q.sql, params: q.params }; +} + +interface DeleteCapture { + table: string; + where: SQL; +} + +function makeDb(opts: { failFor?: string[] } = {}) { + const deletes: DeleteCapture[] = []; + const db = { + delete: (table: unknown) => { + const name = getTableName(table as Parameters[0]); + return { + where: async (where: SQL) => { + if (opts.failFor?.includes(name)) throw new Error(`boom:${name}`); + deletes.push({ table: name, where }); + return { rowsAffected: 0 }; + }, + }; + }, + } as unknown as DB; + return { db, deletes }; +} + +/** 캡처된 delete 목록에서 테이블명으로 where 를 찾아 렌더링한다. */ +function whereFor(deletes: DeleteCapture[], table: string): { sql: string; params: unknown[] } { + const cap = deletes.find((d) => d.table === table); + if (!cap) throw new Error(`delete for ${table} 미실행`); + return render(cap.where); +} + +// runExpiredDataGc 는 진행 로그를 console 에 남긴다 — 테스트 출력 소음 억제. +let logSpy: ReturnType; +let errSpy: ReturnType; +beforeEach(() => { + logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); +}); +afterEach(() => { + logSpy.mockRestore(); + errSpy.mockRestore(); +}); + +const ALL_TABLES = [ + "oidc_refresh_tokens", + "webauthn_challenges", + "rate_limits", + "oidc_grants", + "password_reset_tokens", + "email_verification_tokens", + "saml_slo_states", + "saml_authn_request_ids", + "saml_sessions", + "sessions", +]; + +describe("runExpiredDataGc — 보수적 만료 조건", () => { + it("모든 대상 테이블에 대해 정확히 한 번씩 DELETE 를 실행한다", async () => { + const { db, deletes } = makeDb(); + const result = await runExpiredDataGc(db); + expect(deletes.map((d) => d.table).sort()).toEqual([...ALL_TABLES].sort()); + expect(result.tables.filter((t) => t.ok).length).toBe(ALL_TABLES.length); + }); + + it("sessions: expiresAt 이 refresh TTL(30일) 유예를 넘긴 세션만 삭제한다", async () => { + const start = Date.now(); + const { db, deletes } = makeDb(); + await runExpiredDataGc(db); + const end = Date.now(); + + const { sql, params } = whereFor(deletes, "sessions"); + expect(sql).toContain('"sessions"."expires_at" < ?'); + // cutoff = now - 30일. (sqlite timestamp_ms 컬럼 → 파라미터는 epoch ms 숫자) + const cutoff = params[0] as number; + expect(typeof cutoff).toBe("number"); + expect(cutoff).toBeLessThanOrEqual(end - REFRESH_TOKEN_TTL_MS); + expect(cutoff).toBeGreaterThanOrEqual(start - REFRESH_TOKEN_TTL_MS - 5_000); + }); + + it("saml_authn_request_ids: 유예 없이 expiresAt 경과분만 삭제한다(replay 창 보존)", async () => { + const start = Date.now(); + const { db, deletes } = makeDb(); + await runExpiredDataGc(db); + const end = Date.now(); + + const { sql, params } = whereFor(deletes, "saml_authn_request_ids"); + expect(sql).toContain('"saml_authn_request_ids"."expires_at" < ?'); + const cutoff = params[0] as number; + // 유예가 없으므로 cutoff ≈ now (sessions 의 30일 소급과 대비). + expect(cutoff).toBeGreaterThanOrEqual(start - 5_000); + expect(cutoff).toBeLessThanOrEqual(end); + + // sessions cutoff 는 약 30일 더 과거여야 한다(유예 존재 대비 검증). + const sessCutoff = whereFor(deletes, "sessions").params[0] as number; + expect(cutoff - sessCutoff).toBeGreaterThanOrEqual(REFRESH_TOKEN_TTL_MS - 5_000); + }); + + it("email_verification_tokens: 유예 없이 expiresAt 경과분만 삭제한다", async () => { + const start = Date.now(); + const { db, deletes } = makeDb(); + await runExpiredDataGc(db); + const end = Date.now(); + + const { sql, params } = whereFor(deletes, "email_verification_tokens"); + expect(sql).toContain('"email_verification_tokens"."expires_at" < ?'); + const cutoff = params[0] as number; + expect(cutoff).toBeGreaterThanOrEqual(start - 5_000); + expect(cutoff).toBeLessThanOrEqual(end); + }); + + it("saml_sessions: 활성 세션(endedAt NULL)을 삭제하지 않고, notOnOrAfter/endedAt 유예 경과분만 삭제한다", async () => { + const start = Date.now(); + const { db, deletes } = makeDb(); + await runExpiredDataGc(db); + const end = Date.now(); + + const { sql, params } = whereFor(deletes, "saml_sessions"); + // 두 분기(OR): 만료창(notOnOrAfter) 또는 로그아웃(endedAt) 이 유예를 넘긴 경우. + expect(sql).toContain('"saml_sessions"."not_on_or_after" < ?'); + expect(sql).toContain('"saml_sessions"."ended_at" < ?'); + expect(sql).toContain(" or "); + // endedAt 은 오직 `<` 비교로만 쓰인다 → SQL 시맨틱상 NULL(활성) 은 절대 매칭되지 않는다. + // (is null / is not null 스윕이 없어야 활성 세션이 성급히 삭제되지 않는다.) + expect(sql).not.toContain('"saml_sessions"."ended_at" is'); + + // 두 분기 모두 IdP 세션 TTL(12h) 유예 cutoff = now - SESSION_TTL_MS. + const [c1, c2] = params as number[]; + expect(c1).toBe(c2); + expect(c1).toBeLessThanOrEqual(end - SESSION_TTL_MS); + expect(c1).toBeGreaterThanOrEqual(start - SESSION_TTL_MS - 5_000); + }); + + it("oidc_grants / password_reset_tokens / saml_slo_states: expiresAt 경과분만 삭제한다", async () => { + const { db, deletes } = makeDb(); + await runExpiredDataGc(db); + for (const table of ["oidc_grants", "password_reset_tokens", "saml_slo_states"]) { + const { sql } = whereFor(deletes, table); + expect(sql, table).toContain(`"${table}"."expires_at" < ?`); + } + }); +}); + +describe("runExpiredDataGc — 테이블별 에러 격리", () => { + it("한 테이블의 실패가 다른 테이블의 purge/delete 를 막지 않는다", async () => { + // purge 함수 대상(oidc_refresh_tokens) 과 직접 delete 대상(saml_sessions) 을 동시에 실패시킨다. + const failing = ["oidc_refresh_tokens", "saml_sessions"]; + const { db, deletes } = makeDb({ failFor: failing }); + const result = await runExpiredDataGc(db); + + // 실패한 테이블은 결과에 ok:false + error 로 기록된다. + for (const t of failing) { + const entry = result.tables.find((r) => r.table === t); + expect(entry?.ok).toBe(false); + expect(entry?.error).toContain("boom"); + } + + // 나머지 테이블은 모두 정상 실행(캡처됨 + ok:true)돼야 한다. + const survivors = ALL_TABLES.filter((t) => !failing.includes(t)); + for (const t of survivors) { + expect( + deletes.some((d) => d.table === t), + `${t} 실행됨`, + ).toBe(true); + expect(result.tables.find((r) => r.table === t)?.ok).toBe(true); + } + + // 결과에는 여전히 모든 테이블 항목이 존재한다. + expect(result.tables.map((r) => r.table).sort()).toEqual([...ALL_TABLES].sort()); + }); +}); diff --git a/test/unit/oidc-grant.test.ts b/test/unit/oidc-grant.test.ts new file mode 100644 index 0000000..18fec18 --- /dev/null +++ b/test/unit/oidc-grant.test.ts @@ -0,0 +1,124 @@ +import { describe, it, expect } from "vitest"; +import type { DB } from "$lib/server/db"; +import { createGrant, findAndConsumeGrant, type CreateGrantParams } from "$lib/server/oidc/grant"; + +// ── mock 설계 요약 ──────────────────────────────────────────────────────────── +// grant.ts 는 활성 방언(테스트 = d1)에서 아래 두 호출 shape 만 쓴다: +// createGrant : db.insert(oidcGrants).values({...}) (await) +// findAndConsumeGrant : db.update(oidcGrants).set({usedAt}).where(..) (원자적 claim) +// .returning() (await → 소진된 row[]) +// 실제 계약과 어긋나지 않도록 정확히 이 체인만 mock 하고, insert payload 와 +// returning 결과를 캡처/주입한다. WHERE 가드(codeHash·tenant·client·isNull(usedAt)· +// gt(expiresAt))는 DB 가 강제하므로, mock 은 "claim 매칭 여부"를 returning 배열의 +// 존재/부재로 표현한다 — 만료·이미소진·해시불일치는 모두 매칭 실패(빈 배열)로 귀결된다. +function makeDb(returningRows: unknown[] = []) { + const inserts: Record[] = []; + const updateSets: Record[] = []; + const db = { + insert: () => ({ + values: async (v: Record) => { + inserts.push(v); + }, + }), + update: () => ({ + set: (v: Record) => { + updateSets.push(v); + return { + where: () => ({ + returning: async () => returningRows, + }), + }; + }, + }), + }; + return { db: db as unknown as DB, inserts, updateSets }; +} + +// grant.ts 의 sha256Base64Url 와 동일 산식 — codeHash 계약 검증용. +async function sha256Base64Url(input: string): Promise { + const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input)); + let bin = ""; + for (const b of new Uint8Array(buf)) bin += String.fromCharCode(b); + return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +function grantParams(overrides: Partial = {}): CreateGrantParams { + return { + tenantId: "t1", + clientId: "c1", + userId: "u1", + sessionId: "s1", + code: "raw-authorization-code-xyz", + codeChallenge: "chal", + codeChallengeMethod: "S256", + redirectUri: "https://rp.example/cb", + scope: "openid profile", + nonce: "n1", + state: "st1", + ...overrides, + }; +} + +describe("createGrant — codeHash 기반 저장(평문 code 미저장)", () => { + it("raw code 는 저장하지 않고 SHA-256 base64url 해시(codeHash)만 insert", async () => { + const { db, inserts } = makeDb(); + const params = grantParams(); + await createGrant(db, params); + + expect(inserts.length).toBe(1); + const v = inserts[0]; + // 평문 code 컬럼은 insert 페이로드에 존재하지 않아야 한다. + expect(v.code).toBeUndefined(); + // codeHash 는 raw code 의 SHA-256 base64url 과 정확히 일치. + expect(v.codeHash).toBe(await sha256Base64Url(params.code)); + expect(v.codeHash).not.toBe(params.code); + // 나머지 필드 passthrough + 서버 생성 값. + expect(typeof v.id).toBe("string"); + expect(v.tenantId).toBe("t1"); + expect(v.clientId).toBe("c1"); + expect(v.redirectUri).toBe("https://rp.example/cb"); + expect(v.expiresAt).toBeInstanceOf(Date); + }); + + it("expiresAt 는 now+5분(AUTH_CODE_TTL) 근방", async () => { + const { db, inserts } = makeDb(); + const before = Date.now(); + await createGrant(db, grantParams()); + const exp = (inserts[0].expiresAt as Date).getTime(); + expect(exp).toBeGreaterThanOrEqual(before + 5 * 60 * 1000 - 2000); + expect(exp).toBeLessThanOrEqual(Date.now() + 5 * 60 * 1000 + 2000); + }); +}); + +describe("findAndConsumeGrant — 원자적 1회 소진(d1 RETURNING)", () => { + it("claim 성공(RETURNING 이 row 반환) 시 grant 반환 + usedAt 세팅 UPDATE 발행", async () => { + const grant = { id: "g1", tenantId: "t1", clientId: "c1", scope: "openid" }; + const { db, updateSets } = makeDb([grant]); + + const res = await findAndConsumeGrant(db, "t1", "c1", "raw-authorization-code-xyz"); + expect(res).toBe(grant); + // 원자적 claim: usedAt 을 세팅하는 UPDATE 가 정확히 한 번 발행됐다. + expect(updateSets.length).toBe(1); + expect(updateSets[0].usedAt).toBeInstanceOf(Date); + }); + + it("만료 grant: WHERE 가드(gt expiresAt)로 claim 미매칭 → RETURNING 빈 배열 → null", async () => { + const { db, updateSets } = makeDb([]); // 매칭 row 없음 + const res = await findAndConsumeGrant(db, "t1", "c1", "expired-code"); + expect(res).toBeNull(); + // 소진 UPDATE 자체는 시도된다(원자적 claim). 매칭 0행이라 RETURNING 이 비어 null. + expect(updateSets.length).toBe(1); + }); + + it("이미 소진된 grant: WHERE 가드(isNull usedAt)로 claim 미매칭 → null", async () => { + const { db } = makeDb([]); + const res = await findAndConsumeGrant(db, "t1", "c1", "already-used-code"); + expect(res).toBeNull(); + }); + + it("해시 불일치(다른 code): 매칭 row 없음 → null", async () => { + const { db } = makeDb([]); + const res = await findAndConsumeGrant(db, "t1", "c1", "unknown-code"); + expect(res).toBeNull(); + }); +}); diff --git a/test/unit/oidc-refresh.test.ts b/test/unit/oidc-refresh.test.ts new file mode 100644 index 0000000..9608e72 --- /dev/null +++ b/test/unit/oidc-refresh.test.ts @@ -0,0 +1,218 @@ +import { describe, it, expect } from "vitest"; +import type { DB } from "$lib/server/db"; +import { issueRefreshToken, revokeRefreshTokenFamily, rotateRefreshToken } from "$lib/server/oidc/refresh"; + +// ── mock 설계 요약 ──────────────────────────────────────────────────────────── +// refresh.ts 가 활성 방언(테스트 = d1)에서 쓰는 호출 shape: +// issueRefreshToken : db.insert(tbl).values({...}) (await) +// revokeRefreshTokenFamily: db.update(tbl).set({revokedAt}).where(..) (await) +// rotateRefreshToken : +// 1) db.select().from(tbl).where(..).limit(1) → [record?] +// 2) runAtomic(db,[claim,insert]) → d1 경로에서 db.batch([...]) +// claim = db.update(tbl).set({revokedAt,replacedById}).where(..).returning({id}) +// insert = db.insert(tbl).values({...}) +// batch 결과[0] 의 length>0 이면 claim 승자. +// +// 실제 계약 대응: +// - .where() 는 revokeFamily 처럼 직접 await 될 수도(→ thenable), rotate claim 처럼 +// .returning() 이 이어질 수도 있어 둘 다 지원한다. +// - .values()/.returning() 은 batch 에 수집될 때 __result 로 실행 결과를 노출하고, +// 직접 await 될 때(thenable)도 동작한다. batch 는 수집된 빌더의 __result 를 +// op 순서대로 돌려줘 runAtomic 계약(결과 배열=op 순서)을 재현한다. +interface MakeDbOpts { + selectResult?: unknown[]; + claimReturning?: Array<{ id: string }>; +} +function makeDb(opts: MakeDbOpts = {}) { + const inserts: Record[] = []; + const updateSets: Record[] = []; + let batchCalls = 0; + const selectResult = opts.selectResult ?? []; + const claimReturning = opts.claimReturning ?? []; + + const db = { + select: () => ({ + from: () => ({ + where: () => ({ + limit: async () => selectResult, + }), + }), + }), + insert: () => ({ + values: (v: Record) => { + inserts.push(v); + // batch 수집(__result) + 직접 await(thenable) 양쪽 지원. + return { __result: undefined, then: (res: (x: unknown) => void) => res(undefined) }; + }, + }), + update: () => ({ + set: (v: Record) => { + updateSets.push(v); + return { + where: () => ({ + // 직접 await(revokeFamily) 경로. + then: (res: (x: unknown) => void) => res(undefined), + // batch claim 경로. + returning: () => ({ __result: claimReturning, then: (res: (x: unknown) => void) => res(claimReturning) }), + }), + }; + }, + }), + batch: async (builders: Array<{ __result: unknown }>) => { + batchCalls += 1; + return builders.map((b) => b.__result); + }, + }; + return { + db: db as unknown as DB, + inserts, + updateSets, + get batchCalls() { + return batchCalls; + }, + }; +} + +async function sha256Hex(input: string): Promise { + const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input)); + return Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join(""); +} + +const URLSAFE_B64 = /^[A-Za-z0-9_-]+$/; + +function activeRecord(overrides: Record = {}) { + return { + id: "rt-old", + tenantId: "t1", + clientId: "c1", + userId: "u1", + sessionId: "sess-1", + scope: "openid offline_access", + tokenHash: "oldhash", + revokedAt: null, + replacedById: null, + expiresAt: new Date(Date.now() + 60_000), + ...overrides, + }; +} + +describe("issueRefreshToken — raw 토큰 미저장(해시만 보관)", () => { + it("32바이트 URL-safe base64 토큰 반환 + SHA-256 해시만 insert", async () => { + const { db, inserts } = makeDb(); + const before = Date.now(); + const token = await issueRefreshToken(db, { tenantId: "t1", clientId: "c1", userId: "u1", sessionId: "sess-1", scope: "openid" }); + + expect(URLSAFE_B64.test(token)).toBe(true); + expect(token.length).toBe(43); // 32바이트 base64url(패딩 제거) + + expect(inserts.length).toBe(1); + const v = inserts[0]; + expect(v.token).toBeUndefined(); // 평문 토큰 컬럼 없음 + expect(v.tokenHash).toBe(await sha256Hex(token)); + expect(v.tokenHash).not.toBe(token); + expect(v.tenantId).toBe("t1"); + expect(v.clientId).toBe("c1"); + expect(v.userId).toBe("u1"); + expect(v.sessionId).toBe("sess-1"); + expect(v.scope).toBe("openid"); + expect(typeof v.id).toBe("string"); + const exp = (v.expiresAt as Date).getTime(); + expect(exp).toBeGreaterThanOrEqual(before + 30 * 24 * 60 * 60 * 1000 - 2000); + }); +}); + +describe("revokeRefreshTokenFamily", () => { + it("revokedAt 을 세팅하는 UPDATE 를 발행", async () => { + const { db, updateSets } = makeDb(); + await revokeRefreshTokenFamily(db, "t1", "u1", "c1"); + expect(updateSets.length).toBe(1); + expect(updateSets[0].revokedAt).toBeInstanceOf(Date); + }); +}); + +describe("rotateRefreshToken — 회전 성공 경로(runAtomic claim+insert)", () => { + it("claim 승자 → { ok:true } + 새 토큰 발급, 새 토큰 해시만 insert", async () => { + const record = activeRecord(); + const store = makeDb({ selectResult: [record], claimReturning: [{ id: "rt-new" }] }); + const { db, inserts, updateSets } = store; + + const res = await rotateRefreshToken(db, "t1", "c1", "presented-raw-token"); + expect(res.ok).toBe(true); + if (!res.ok) throw new Error("unreachable"); + expect(res.record).toBe(record); + expect(URLSAFE_B64.test(res.newToken)).toBe(true); + expect(res.newToken.length).toBe(43); + + // 원자적 batch 가 한 번 실행됐다. + expect(store.batchCalls).toBe(1); + + // claim UPDATE: revokedAt + replacedById 세팅(회전 링크). + expect(updateSets.length).toBe(1); + expect(updateSets[0].revokedAt).toBeInstanceOf(Date); + expect(updateSets[0].replacedById).toBeDefined(); + + // new 토큰은 해시로만 저장 — 평문 미저장, 원본 raw != 해시. + expect(inserts.length).toBe(1); + expect(inserts[0].token).toBeUndefined(); + expect(inserts[0].tokenHash).toBe(await sha256Hex(res.newToken)); + expect(inserts[0].userId).toBe("u1"); + }); +}); + +describe("rotateRefreshToken — 거부/재사용 경로", () => { + it("없는 토큰 → { ok:false, reason:'invalid_grant' }, batch/insert 없음", async () => { + const store = makeDb({ selectResult: [] }); + const res = await rotateRefreshToken(store.db, "t1", "c1", "nope"); + expect(res).toEqual({ ok: false, reason: "invalid_grant" }); + expect(store.batchCalls).toBe(0); + expect(store.inserts.length).toBe(0); + }); + + it("이미 revoke 된 토큰 재사용 → family 폐기 + { reason:'reuse' } (회전 없음)", async () => { + const record = activeRecord({ revokedAt: new Date(Date.now() - 1000) }); + const store = makeDb({ selectResult: [record] }); + const { updateSets } = store; + const res = await rotateRefreshToken(store.db, "t1", "c1", "reused"); + expect(res).toEqual({ ok: false, reason: "reuse" }); + // 회전 batch 는 실행되지 않고, family 폐기 UPDATE 만 발행. + expect(store.batchCalls).toBe(0); + expect(store.inserts.length).toBe(0); + expect(updateSets.length).toBe(1); + expect(updateSets[0].revokedAt).toBeInstanceOf(Date); + expect(updateSets[0].replacedById).toBeUndefined(); // family 폐기는 revokedAt 만 + }); + + it("revoked 이지만 미만료인 토큰도 재사용으로 감지(만료보다 revoke 우선 판정)", async () => { + // revokedAt 검사가 expiresAt 검사보다 먼저 — 미만료여도 reuse 로 귀결되어야 한다. + const record = activeRecord({ revokedAt: new Date(Date.now() - 1000), expiresAt: new Date(Date.now() + 60_000) }); + const store = makeDb({ selectResult: [record] }); + const res = await rotateRefreshToken(store.db, "t1", "c1", "reused-not-expired"); + expect(res).toEqual({ ok: false, reason: "reuse" }); + expect(store.batchCalls).toBe(0); + }); + + it("만료 토큰(미revoke) → { reason:'expired' }, family 폐기/회전 없음", async () => { + const record = activeRecord({ revokedAt: null, expiresAt: new Date(Date.now() - 1000) }); + const store = makeDb({ selectResult: [record] }); + const res = await rotateRefreshToken(store.db, "t1", "c1", "expired"); + expect(res).toEqual({ ok: false, reason: "expired" }); + expect(store.batchCalls).toBe(0); + expect(store.updateSets.length).toBe(0); + }); + + it("동시 회전 패자(claim 0행) → family 폐기 + { reason:'reuse' }", async () => { + // 유효 토큰이지만 claim UPDATE 가 0행(다른 요청이 먼저 회전) → 재사용으로 간주. + const record = activeRecord(); + const store = makeDb({ selectResult: [record], claimReturning: [] }); + const { updateSets } = store; + const res = await rotateRefreshToken(store.db, "t1", "c1", "concurrent-loser"); + expect(res).toEqual({ ok: false, reason: "reuse" }); + expect(store.batchCalls).toBe(1); // batch 는 실행됨(패자도 insert 시도) + expect(store.inserts.length).toBe(1); // 패자의 new 토큰도 삽입되지만 반환되지 않음 + // updateSets: [0]=claim(replacedById 有), [1]=family 폐기(revokedAt 만) + expect(updateSets.length).toBe(2); + expect(updateSets[0].replacedById).toBeDefined(); + expect(updateSets[1].replacedById).toBeUndefined(); + expect(updateSets[1].revokedAt).toBeInstanceOf(Date); + }); +}); diff --git a/test/unit/password.test.ts b/test/unit/password.test.ts new file mode 100644 index 0000000..e61e5fc --- /dev/null +++ b/test/unit/password.test.ts @@ -0,0 +1,158 @@ +import { describe, it, expect } from "vitest"; +import { hashEncoded, Config } from "@hicaru/argon2-pure.js"; +import { hashPassword, verifyPassword, timingSafeEqual } from "$lib/server/auth/password"; + +// 운영 scrypt 파라미터 (password.ts 와 동일해야 함) — 교차 검증용 상수. +const OP_N = 32768; +const OP_R = 8; +const OP_P = 3; + +function bytesFromB64(b64: string): Uint8Array { + return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); +} +function b64FromBytes(bytes: Uint8Array): string { + return btoa(String.fromCharCode(...bytes)); +} + +// scrypt$N=..,r=..,p=..$saltB64$hashB64 레코드를 파싱한다. +function parseScryptRecord(record: string) { + const parts = record.split("$"); + expect(parts).toHaveLength(4); + expect(parts[0]).toBe("scrypt"); + const params: Record = {}; + for (const pair of parts[1].split(",")) { + const [k, v] = pair.split("="); + params[k] = Number(v); + } + return { params, salt: bytesFromB64(parts[2]), hash: bytesFromB64(parts[3]) }; +} + +describe("scrypt 해시 (hashPassword / verifyPassword)", () => { + it("해시 → 검증 라운드트립 (현행 파라미터는 rehash 없음)", async () => { + const record = await hashPassword("hunter2"); + const result = await verifyPassword("hunter2", record); + expect(result.valid).toBe(true); + expect(result.rehash).toBeUndefined(); + }); + + it("오답 거부", async () => { + const record = await hashPassword("hunter2"); + expect(await verifyPassword("wrong-password", record)).toEqual({ valid: false }); + }); + + it("레코드 형식: 운영 파라미터(N=32768,r=8,p=3), salt 16B, derived 32B", async () => { + const record = await hashPassword("pw"); + expect(record.startsWith("scrypt$N=32768,r=8,p=3$")).toBe(true); + const { params, salt, hash } = parseScryptRecord(record); + expect(params).toEqual({ N: OP_N, r: OP_R, p: OP_P }); + expect(salt).toHaveLength(16); + expect(hash).toHaveLength(32); + }); + + it("같은 비밀번호라도 salt 랜덤화로 매번 다른 해시", async () => { + const a = await hashPassword("same"); + const b = await hashPassword("same"); + expect(a).not.toBe(b); + // 그럼에도 둘 다 검증 통과 + expect((await verifyPassword("same", a)).valid).toBe(true); + expect((await verifyPassword("same", b)).valid).toBe(true); + }); + + it("손상된 scrypt 파라미터(N 비-2의거듭제곱) 거부", async () => { + const { salt, hash } = parseScryptRecord(await hashPassword("pw")); + const bad = `scrypt$N=1000,r=8,p=3$${b64FromBytes(salt)}$${b64FromBytes(hash)}`; + expect(await verifyPassword("pw", bad)).toEqual({ valid: false }); + }); + + it("과대 파라미터(N > 2^17) 거부 — 메모리 폭탄 방어", async () => { + const { salt, hash } = parseScryptRecord(await hashPassword("pw")); + const bad = `scrypt$N=262144,r=8,p=3$${b64FromBytes(salt)}$${b64FromBytes(hash)}`; + expect(await verifyPassword("pw", bad)).toEqual({ valid: false }); + }); +}); + +describe("상수시간 비교 (timingSafeEqual)", () => { + it("동일 바이트열 true, 상이 false, 길이 불일치 false", () => { + const a = new Uint8Array([1, 2, 3, 4]); + expect(timingSafeEqual(a, new Uint8Array([1, 2, 3, 4]))).toBe(true); + expect(timingSafeEqual(a, new Uint8Array([1, 2, 3, 5]))).toBe(false); + expect(timingSafeEqual(a, new Uint8Array([1, 2, 3]))).toBe(false); + }); +}); + +describe("레거시 PBKDF2 검증 → scrypt 업그레이드", () => { + async function makePbkdf2(password: string, iterations: number, digestLabel: string): Promise { + const salt = crypto.getRandomValues(new Uint8Array(16)); + const keyMaterial = await crypto.subtle.importKey("raw", new TextEncoder().encode(password), { name: "PBKDF2" }, false, ["deriveBits"]); + // verifyPbkdf2 는 항상 SHA-256 으로 재파생하므로 정답 해시는 SHA-256 기반. + const bits = await crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt: salt.buffer as ArrayBuffer, iterations }, keyMaterial, 256); + return `pbkdf2$${digestLabel}:${iterations}$${b64FromBytes(salt)}$${b64FromBytes(new Uint8Array(bits))}`; + } + + it("유효 pbkdf2(sha256, 100000) → valid + scrypt rehash", async () => { + const record = await makePbkdf2("legacy-pw", 100_000, "sha256"); + const result = await verifyPassword("legacy-pw", record); + expect(result.valid).toBe(true); + expect(result.rehash?.startsWith("scrypt$")).toBe(true); + }); + + it("오답 거부", async () => { + const record = await makePbkdf2("legacy-pw", 100_000, "sha256"); + expect(await verifyPassword("nope", record)).toEqual({ valid: false }); + }); + + it("iteration 하한(100000) 미만 거부", async () => { + const record = await makePbkdf2("legacy-pw", 99_999, "sha256"); + expect(await verifyPassword("legacy-pw", record)).toEqual({ valid: false }); + }); + + it("digest 불일치(sha512 라벨) 거부", async () => { + // 해시는 SHA-256 으로 만들었지만 라벨이 sha512 → digest 명시 일치 실패로 거부 + const record = await makePbkdf2("legacy-pw", 100_000, "sha512"); + expect(await verifyPassword("legacy-pw", record)).toEqual({ valid: false }); + }); +}); + +describe("레거시 argon2id 검증 → scrypt 업그레이드", () => { + function makeArgon2id(password: string): string { + // 테스트 속도용 저강도 파라미터. 검증 경로(verifyEncoded)만 확인하면 충분. + const cfg = new Config(); + cfg.memCost = 64; + cfg.timeCost = 1; + cfg.lanes = 1; + cfg.hashLength = 16; + const salt = new TextEncoder().encode("test-salt-16byte"); + return hashEncoded(new TextEncoder().encode(password), salt, cfg); + } + + it("유효 argon2id → valid + scrypt rehash", async () => { + const record = makeArgon2id("legacy-pw"); + expect(record.startsWith("$argon2id$")).toBe(true); + const result = await verifyPassword("legacy-pw", record); + expect(result.valid).toBe(true); + expect(result.rehash?.startsWith("scrypt$")).toBe(true); + }); + + it("오답 거부", async () => { + const record = makeArgon2id("legacy-pw"); + expect(await verifyPassword("nope", record)).toEqual({ valid: false }); + }); +}); + +describe("TIMING_DUMMY_HASH (users.ts, S1 타이밍 균등화 상수)", () => { + // users.ts 는 DB 의존이라 import 대상 외 — 상수 문자열만 복제해 형식을 교차 검증한다. + const TIMING_DUMMY_HASH = "scrypt$N=32768,r=8,p=3$laGnY6fbAMkDKdFTKRUGyg==$Jm6an31vv6UDMaa2dn2B2riImIX6qmwMUcc6BWcccg8="; + + it("운영 scrypt 파라미터와 일치하는 유효 레코드 (salt 16B, hash 32B)", () => { + const { params, salt, hash } = parseScryptRecord(TIMING_DUMMY_HASH); + expect(params).toEqual({ N: OP_N, r: OP_R, p: OP_P }); + expect(salt).toHaveLength(16); + expect(hash).toHaveLength(32); + }); + + it("verifyPassword 가 파싱/파생 가능한 레코드로 동작 (임의 입력은 불일치)", async () => { + // 실제 scrypt 파생 1회를 태우되(타이밍 균등화 목적) 결과는 valid:false 여야 한다. + const result = await verifyPassword("any-attacker-guess", TIMING_DUMMY_HASH); + expect(result).toEqual({ valid: false }); + }); +}); diff --git a/test/unit/ratelimit.test.ts b/test/unit/ratelimit.test.ts new file mode 100644 index 0000000..85f1f58 --- /dev/null +++ b/test/unit/ratelimit.test.ts @@ -0,0 +1,141 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import type { DB } from "$lib/server/db"; +import { checkRateLimit } from "$lib/server/ratelimit/index"; + +// ── mock 설계 요약 ──────────────────────────────────────────────────────────── +// checkRateLimit 가 활성 방언(테스트 = d1)에서 쓰는 호출 shape: +// 1) 현재 버킷 upsert: +// db.insert(rateLimits).values({key,count,expiresAt}) +// .onConflictDoUpdate({target,set}).returning({count}) → [{count}] +// 2) 이전 버킷 조회: +// db.select({count}).from(rateLimits).where(eq(key,prevKey)).limit(1) → [{count}?] +// +// 실제 계약 대응: +// - upsert 의 returning 결과로 currentCount 를 주입한다(원자적 증가 후 카운트). +// - select 결과로 prevCount 를 주입한다(이전 윈도우 버킷). +// - insertValues 를 캡처해 currentKey(`key:windowIndex`)·expiresAt 를 검증한다. +// 시간은 vi.useFakeTimers 로 고정해 windowIndex·elapsed 를 결정론적으로 만든다. +interface MakeDbOpts { + currentCount: number; + prevCount?: number; +} +function makeDb(opts: MakeDbOpts) { + let insertValues: Record | undefined; + const db = { + insert: () => ({ + values: (v: Record) => { + insertValues = v; + return { + onConflictDoUpdate: () => ({ + returning: async () => [{ count: opts.currentCount }], + }), + }; + }, + }), + select: () => ({ + from: () => ({ + where: () => ({ + limit: async () => (opts.prevCount === undefined ? [] : [{ count: opts.prevCount }]), + }), + }), + }), + }; + return { + db: db as unknown as DB, + get insertValues() { + return insertValues; + }, + }; +} + +// now=90000, windowMs=60000 → windowIndex=1, windowStart=60000, elapsed=30000 (윈도우 절반) +const NOW = 90_000; +const WINDOW = 60_000; + +beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(NOW)); +}); +afterEach(() => { + vi.useRealTimers(); +}); + +describe("checkRateLimit — 한도 내/초과", () => { + it("한도 내: allowed=true, remaining=limit-sliding, retryAfter=0", async () => { + // prev=0, current=3 → sliding = floor(0*0.5)+3 = 3 ≤ 5 + const { db } = makeDb({ currentCount: 3, prevCount: 0 }); + const res = await checkRateLimit(db, "user:a", { windowMs: WINDOW, limit: 5 }); + expect(res.allowed).toBe(true); + expect(res.remaining).toBe(2); + expect(res.retryAfterMs).toBe(0); + }); + + it("초과: allowed=false, remaining=0, retryAfterMs=windowMs-elapsed", async () => { + // prev=0, current=6 → sliding = 6 > 5 + const { db } = makeDb({ currentCount: 6, prevCount: 0 }); + const res = await checkRateLimit(db, "user:a", { windowMs: WINDOW, limit: 5 }); + expect(res.allowed).toBe(false); + expect(res.remaining).toBe(0); + // elapsed = 30000 → retryAfter = 60000-30000 = 30000 + expect(res.retryAfterMs).toBe(WINDOW - 30_000); + }); +}); + +describe("checkRateLimit — 슬라이딩 윈도우 감쇠 산식", () => { + it("floor(prev*(1-elapsed/window)) + current 로 이전 버킷을 가중 감쇠", async () => { + // elapsed=30000, factor=1-0.5=0.5. prev=4, current=1 + // sliding = floor(4*0.5)+1 = 2+1 = 3 ≤ 5 → 허용, remaining=2 + const { db } = makeDb({ currentCount: 1, prevCount: 4 }); + const res = await checkRateLimit(db, "user:a", { windowMs: WINDOW, limit: 5 }); + expect(res.allowed).toBe(true); + // remaining 이 current(1)만이 아니라 감쇠된 prev(2)까지 반영해야 진짜 슬라이딩. + expect(res.remaining).toBe(2); + }); + + it("감쇠된 prev 가중치로 한도를 넘으면 차단", async () => { + // prev=10, current=1, factor=0.5 → floor(5)+1 = 6 > 5 → 차단 + const { db } = makeDb({ currentCount: 1, prevCount: 10 }); + const res = await checkRateLimit(db, "user:a", { windowMs: WINDOW, limit: 5 }); + expect(res.allowed).toBe(false); + expect(res.remaining).toBe(0); + }); + + it("elapsed 비율에 따라 감쇠 강도가 달라진다(윈도우 초반엔 prev 가중치 큼)", async () => { + // now=61000 → elapsed=1000, factor≈1-1000/60000. prev=5, current=1 + // floor(5*(1-1000/60000))+1 = floor(4.916..)+1 = 4+1 = 5 ≤ 5 → 허용(경계) + vi.setSystemTime(new Date(61_000)); + const { db } = makeDb({ currentCount: 1, prevCount: 5 }); + const res = await checkRateLimit(db, "user:a", { windowMs: WINDOW, limit: 5 }); + expect(res.allowed).toBe(true); + expect(res.remaining).toBe(0); + }); +}); + +describe("checkRateLimit — 키 네임스페이싱/버킷 만료", () => { + it("current 버킷 key = `${key}:${windowIndex}`, expiresAt = windowStart + windowMs*2", async () => { + const store = makeDb({ currentCount: 1, prevCount: 0 }); + await checkRateLimit(store.db, "user:a", { windowMs: WINDOW, limit: 5 }); + expect(store.insertValues?.key).toBe("user:a:1"); // windowIndex = floor(90000/60000) = 1 + expect(store.insertValues?.count).toBe(1); + // windowStart(60000) + windowMs*2(120000) = 180000 + expect((store.insertValues?.expiresAt as Date).getTime()).toBe(60_000 + WINDOW * 2); + }); + + it("서로 다른 키는 서로 다른 버킷 key 로 격리된다", async () => { + const a = makeDb({ currentCount: 1, prevCount: 0 }); + await checkRateLimit(a.db, "user:a", { windowMs: WINDOW, limit: 5 }); + const b = makeDb({ currentCount: 1, prevCount: 0 }); + await checkRateLimit(b.db, "user:b", { windowMs: WINDOW, limit: 5 }); + expect(a.insertValues?.key).toBe("user:a:1"); + expect(b.insertValues?.key).toBe("user:b:1"); + expect(a.insertValues?.key).not.toBe(b.insertValues?.key); + }); + + it("이전 버킷이 없으면(prev row 부재) prevCount=0 으로 취급", async () => { + // prevCount undefined → select 빈 배열 → prevCount=0, sliding=current + const { db } = makeDb({ currentCount: 5 }); + const res = await checkRateLimit(db, "user:a", { windowMs: WINDOW, limit: 5 }); + expect(res.allowed).toBe(true); + expect(res.remaining).toBe(0); // 5-5 + }); +}); diff --git a/test/unit/schema-parity.test.ts b/test/unit/schema-parity.test.ts index 3e35dee..45efc1b 100644 --- a/test/unit/schema-parity.test.ts +++ b/test/unit/schema-parity.test.ts @@ -1,50 +1,252 @@ import { describe, it, expect } from "vitest"; -import { getTableColumns, getTableName, is, Table } from "drizzle-orm"; +import { is } from "drizzle-orm"; +import { SQLiteTable, getTableConfig as getSqliteTableConfig } from "drizzle-orm/sqlite-core"; +import { PgTable, getTableConfig as getPgTableConfig } from "drizzle-orm/pg-core"; +import { MySqlTable, getTableConfig as getMysqlTableConfig } from "drizzle-orm/mysql-core"; import * as sqliteSchema from "../../src/lib/server/db/schema.sqlite"; import * as pgSchema from "../../src/lib/server/db/schema.pg"; import * as mysqlSchema from "../../src/lib/server/db/schema.mysql"; /** - * E1: 3개 방언 스키마(schema.sqlite/pg/mysql)의 테이블·컬럼 집합이 동일한지 강제한다. - * 수작업 동기화 중 한 방언에만 컬럼을 추가/삭제/개명하는 drift 를 CI 에서 잡는다. - * (인덱스 parity 는 방언별 API 차이로 이 테스트 범위 밖 — 별도 수동 검토 필요.) + * E1: 3개 방언 스키마(schema.sqlite/pg/mysql)를 drizzle introspection 으로 정규화해 + * 다음 축의 parity 를 강제한다. 수작업 동기화 중 한 방언에만 생기는 drift 를 CI 에서 잡는다. + * + * 1. 테이블 집합 + * 2. 테이블별 컬럼 집합 + * 3. 컬럼별 nullable(notNull) 여부 + * 4. 컬럼별 타입 "계열"(type family) — 방언 간 정당한 물리 타입 차이는 아래 매핑 테이블로 + * 정규화해 비교하고, 정규화 불가능한 진짜 drift 만 실패시킨다. + * 5. 인덱스·unique 제약 — 인덱스 이름 + unique 여부 + 대상 컬럼 집합. 방언별 정당한 + * 차이는 INDEX_PARITY_EXCEPTIONS 로 명시 문서화하고, 목록에 없는 신규 drift 만 실패. */ -function tableColumnMap(schema: Record): Map { - const out = new Map(); + +// ── 타입 계열(type family) 매핑 테이블 ──────────────────────────────────────────── +// +// 방언 간 정당한 SQL 타입 차이를 하나의 "계열" 로 정규화한다. 키는 drizzle 의 columnType +// (방언별 컬럼 구현 클래스명), 값은 방언 독립 계열 문자열이다. +// +// 예) 타임스탬프 컬럼의 물리 타입은 방언마다 다르지만 모두 "date" 계열이다: +// sqlite integer(mode:timestamp_ms) / pg timestamp(3, tz) / mysql datetime(fsp:3) +// 불리언도 sqlite integer(mode:boolean) / pg boolean / mysql boolean(tinyint) → "boolean". +// 문자열은 sqlite/pg text 와 mysql varchar|text → "string"(mysql 은 인덱싱 위해 길이 필요). +// +// 이 표에 없는 columnType 이 등장하면(=미분류) 아래 컬럼 parity 검사에서 명시적으로 실패해 +// 새 타입을 의도적으로 계열 분류하도록 강제한다(조용한 오분류 방지). +const COLUMN_TYPE_FAMILY: Record = { + // string 계열 + SQLiteText: "string", + PgText: "string", + MySqlVarChar: "string", + MySqlText: "string", + // number 계열 + SQLiteInteger: "number", + PgInteger: "number", + MySqlInt: "number", + // boolean 계열 + SQLiteBoolean: "boolean", + PgBoolean: "boolean", + MySqlBoolean: "boolean", + // date/timestamp 계열 + SQLiteTimestamp: "date", + PgTimestamp: "date", + MySqlDateTime: "date", +}; + +// ── 인덱스 parity 예외 목록 ─────────────────────────────────────────────────────── +// +// 방언별로 정당하게 존재/부재가 갈리는 인덱스를 명시 문서화한다. 여기 등재된 (테이블, +// 인덱스명) 조합은 `missingIn` 에 나열된 방언에서 누락돼 있어도 drift 로 보지 않는다. +// 목록에 없는 신규 누락/추가/컬럼불일치만 실패한다. +interface IndexParityException { + table: string; + index: string; + /** 이 인덱스가 정당하게 누락된 방언들. */ + missingIn: Dialect[]; + reason: string; +} +const INDEX_PARITY_EXCEPTIONS: IndexParityException[] = [ + { + table: "signing_keys", + index: "signing_keys_tenant_one_active_uidx", + missingIn: ["mysql"], + // MySQL 은 partial unique index (WHERE active) 를 지원하지 않는다. sqlite/pg 는 부분 + // 유니크 인덱스로 "tenant 당 active signing key 1개" 불변식을 DB 레벨에서 강제하고, + // mysql 은 애플리케이션 레벨 트랜잭션으로 동일 불변식을 보장한다(schema.mysql.ts 주석). + reason: "MySQL 은 partial unique index 미지원 → 앱 레벨 트랜잭션으로 대체", + }, +]; + +// ── introspection 정규화 ────────────────────────────────────────────────────────── + +type Dialect = "sqlite" | "pg" | "mysql"; + +interface ColumnInfo { + notNull: boolean; + /** 정규화된 타입 계열. 미분류 columnType 이면 `unmapped:`. */ + family: string; +} +interface IndexInfo { + unique: boolean; + /** 대상 컬럼명 정렬 목록(JSON 직렬화용). 표현식 인덱스 항목은 "". */ + columns: string[]; +} +interface TableInfo { + columns: Map; + indexes: Map; +} + +function familyOf(columnType: string): string { + return COLUMN_TYPE_FAMILY[columnType] ?? `unmapped:${columnType}`; +} + +/* eslint-disable @typescript-eslint/no-explicit-any */ +function normalizeTable(cfg: { columns: any[]; indexes: any[] }): TableInfo { + const columns = new Map(); + for (const c of cfg.columns) { + columns.set(c.name, { notNull: Boolean(c.notNull), family: familyOf(c.columnType) }); + } + const indexes = new Map(); + for (const idx of cfg.indexes) { + const conf = idx.config; + const cols = conf.columns.map((col: any) => col?.name ?? "").sort(); + indexes.set(conf.name, { unique: Boolean(conf.unique), columns: cols }); + } + return { columns, indexes }; +} +/* eslint-enable @typescript-eslint/no-explicit-any */ + +function buildTableMap(schema: Record, dialect: Dialect): Map { + const out = new Map(); for (const value of Object.values(schema)) { - if (is(value, Table)) { - const cols = Object.values(getTableColumns(value)) - .map((c) => (c as { name: string }).name) - .sort(); - out.set(getTableName(value), cols); + if (dialect === "sqlite" && is(value, SQLiteTable)) { + const cfg = getSqliteTableConfig(value); + out.set(cfg.name, normalizeTable(cfg)); + } else if (dialect === "pg" && is(value, PgTable)) { + const cfg = getPgTableConfig(value); + out.set(cfg.name, normalizeTable(cfg)); + } else if (dialect === "mysql" && is(value, MySqlTable)) { + const cfg = getMysqlTableConfig(value); + out.set(cfg.name, normalizeTable(cfg)); } } return out; } -const sqlite = tableColumnMap(sqliteSchema as Record); -const pg = tableColumnMap(pgSchema as Record); -const mysql = tableColumnMap(mysqlSchema as Record); +const dialects: { name: Dialect; tables: Map }[] = [ + { name: "sqlite", tables: buildTableMap(sqliteSchema as Record, "sqlite") }, + { name: "pg", tables: buildTableMap(pgSchema as Record, "pg") }, + { name: "mysql", tables: buildTableMap(mysqlSchema as Record, "mysql") }, +]; + +// sqlite 를 기준 방언으로 삼는다(테이블/컬럼 집합 parity 는 아래에서 별도 강제). +const sqlite = dialects[0].tables; +const others = dialects.slice(1); + +function isAllowedIndexOmission(table: string, index: string, dialect: Dialect): boolean { + return INDEX_PARITY_EXCEPTIONS.some((e) => e.table === table && e.index === index && e.missingIn.includes(dialect)); +} describe("schema dialect parity (E1)", () => { it("세 방언의 테이블 집합이 동일", () => { const sqliteTables = [...sqlite.keys()].sort(); - const pgTables = [...pg.keys()].sort(); - const mysqlTables = [...mysql.keys()].sort(); - expect(pgTables).toEqual(sqliteTables); - expect(mysqlTables).toEqual(sqliteTables); + for (const { name, tables } of others) { + expect([...tables.keys()].sort(), `[${name}] 테이블 집합`).toEqual(sqliteTables); + } }); it("각 테이블의 컬럼 집합이 세 방언에서 동일", () => { const mismatches: string[] = []; - for (const [table, cols] of sqlite) { - const pgCols = pg.get(table); - const mysqlCols = mysql.get(table); - if (pgCols && JSON.stringify(pgCols) !== JSON.stringify(cols)) { - mismatches.push(`[pg] ${table}: sqlite=${cols.join(",")} vs pg=${pgCols.join(",")}`); + for (const [table, info] of sqlite) { + const sqliteCols = [...info.columns.keys()].sort(); + for (const { name, tables } of others) { + const cols = [...(tables.get(table)?.columns.keys() ?? [])].sort(); + if (JSON.stringify(cols) !== JSON.stringify(sqliteCols)) { + mismatches.push(`[${name}] ${table}: sqlite=${sqliteCols.join(",")} vs ${name}=${cols.join(",")}`); + } } - if (mysqlCols && JSON.stringify(mysqlCols) !== JSON.stringify(cols)) { - mismatches.push(`[mysql] ${table}: sqlite=${cols.join(",")} vs mysql=${mysqlCols.join(",")}`); + } + expect(mismatches).toEqual([]); + }); + + it("각 컬럼의 nullable(notNull) 여부가 세 방언에서 동일", () => { + const mismatches: string[] = []; + for (const [table, info] of sqlite) { + for (const [col, sc] of info.columns) { + for (const { name, tables } of others) { + const oc = tables.get(table)?.columns.get(col); + if (oc && oc.notNull !== sc.notNull) { + mismatches.push(`[${name}] ${table}.${col}: sqlite.notNull=${sc.notNull} vs ${name}.notNull=${oc.notNull}`); + } + } + } + } + expect(mismatches).toEqual([]); + }); + + it("모든 컬럼의 타입이 매핑 테이블에 분류돼 있다(미분류 columnType 없음)", () => { + const unmapped: string[] = []; + for (const { name, tables } of dialects) { + for (const [table, info] of tables) { + for (const [col, ci] of info.columns) { + if (ci.family.startsWith("unmapped:")) { + unmapped.push(`[${name}] ${table}.${col}: ${ci.family}`); + } + } + } + } + expect(unmapped).toEqual([]); + }); + + it("각 컬럼의 타입 계열(type family)이 세 방언에서 동일", () => { + const mismatches: string[] = []; + for (const [table, info] of sqlite) { + for (const [col, sc] of info.columns) { + for (const { name, tables } of others) { + const oc = tables.get(table)?.columns.get(col); + if (oc && oc.family !== sc.family) { + mismatches.push(`[${name}] ${table}.${col}: sqlite=${sc.family} vs ${name}=${oc.family}`); + } + } + } + } + expect(mismatches).toEqual([]); + }); + + it("인덱스·unique 제약이 세 방언에서 동일(예외 목록 제외)", () => { + const mismatches: string[] = []; + for (const table of sqlite.keys()) { + // 세 방언 중 이 테이블에 존재하는 모든 인덱스 이름의 합집합을 대상으로 비교. + const indexNames = new Set(); + for (const { tables } of dialects) { + for (const idxName of tables.get(table)?.indexes.keys() ?? []) indexNames.add(idxName); + } + + for (const idxName of indexNames) { + // 각 방언의 인덱스 시그니처(없으면 undefined) 수집. + const perDialect = dialects.map(({ name, tables }) => ({ + dialect: name, + idx: tables.get(table)?.indexes.get(idxName), + })); + + // (a) 누락 검사 — 예외 목록에 없는 누락은 drift. + for (const { dialect, idx } of perDialect) { + if (!idx && !isAllowedIndexOmission(table, idxName, dialect)) { + mismatches.push(`[${dialect}] ${table}.${idxName}: 인덱스 누락(예외 목록에 없음)`); + } + } + + // (b) 존재하는 방언들끼리 시그니처(unique + 컬럼 집합) 일치 검사. + const present = perDialect.filter((d) => d.idx); + if (present.length >= 2) { + const ref = present[0].idx!; + const refSig = JSON.stringify({ unique: ref.unique, columns: ref.columns }); + for (const { dialect, idx } of present.slice(1)) { + const sig = JSON.stringify({ unique: idx!.unique, columns: idx!.columns }); + if (sig !== refSig) { + mismatches.push(`[${dialect}] ${table}.${idxName}: ${present[0].dialect}=${refSig} vs ${dialect}=${sig}`); + } + } + } } } expect(mismatches).toEqual([]); diff --git a/test/unit/session.test.ts b/test/unit/session.test.ts new file mode 100644 index 0000000..74abfe2 --- /dev/null +++ b/test/unit/session.test.ts @@ -0,0 +1,225 @@ +import { describe, it, expect } from "vitest"; +import type { SQL } from "drizzle-orm"; +import { SQLiteSyncDialect } from "drizzle-orm/sqlite-core"; +import type { DB } from "$lib/server/db"; +import { createSessionRecord, getSessionContext, touchSession, revokeSession, revokeAllUserSessions, revokeOtherSessions } from "$lib/server/auth/session"; +import { SESSION_TTL_MS } from "$lib/server/auth/constants"; + +// ── 순수 로직 검증용 목(mock) ────────────────────────────────────────────── +// crud-factory.test.ts 패턴을 따른다: 실제 쿼리는 실행하지 않고 insert/update/select +// 체인에 전달된 값·where 조건만 캡처한다. where 는 drizzle SQL 객체이므로, +// SQLiteSyncDialect 로 파라미터화된 SQL 문자열로 렌더링해 컬럼·연산자·바인딩을 검증한다. +const dialect = new SQLiteSyncDialect(); +function render(where: SQL): { sql: string; params: unknown[] } { + const q = dialect.sqlToQuery(where); + return { sql: q.sql, params: q.params }; +} + +interface UpdateCapture { + set: Record; + where: SQL; +} +interface SelectCapture { + on?: SQL; + where?: SQL; +} + +function makeDb(selectResult: unknown[] = []) { + const inserts: Record[] = []; + const updates: UpdateCapture[] = []; + const selects: SelectCapture[] = []; + const db = { + insert: () => ({ + values: async (v: Record) => { + inserts.push(v); + }, + }), + update: () => ({ + set: (set: Record) => ({ + where: async (where: SQL) => { + updates.push({ set, where }); + }, + }), + }), + select: () => ({ + from: () => ({ + innerJoin: (_table: unknown, on: SQL) => ({ + where: (where: SQL) => ({ + limit: async () => { + selects.push({ on, where }); + return selectResult; + }, + }), + }), + }), + }), + } as unknown as DB; + return { db, inserts, updates, selects }; +} + +// 모듈 내부 hashSessionToken 과 동일한 SHA-256 → base64url 산식(검증용 독립 재구현). +function bytesToBase64Url(bytes: Uint8Array): string { + return btoa(String.fromCharCode(...bytes)) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, ""); +} +async function sha256Base64Url(token: string): Promise { + const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token)); + return bytesToBase64Url(new Uint8Array(hash)); +} + +const BASE64URL_32BYTES = /^[A-Za-z0-9_-]{43}$/; + +describe("createSessionRecord", () => { + it("세션 토큰은 SHA-256 해시로만 저장하고 원문 토큰은 DB 에 남기지 않는다", async () => { + const { db, inserts } = makeDb(); + const before = Date.now(); + const { sessionToken, sessionId, expiresAt } = await createSessionRecord(db, { + tenantId: "t1", + userId: "u1", + amr: ["pwd", "totp"], + acr: "acr-mfa", + ip: "1.2.3.4", + userAgent: "ua", + }); + const after = Date.now(); + + expect(inserts.length).toBe(1); + const row = inserts[0]; + + // 저장된 idpSessionId 는 반환된 원문 토큰의 SHA-256(base64url) 해시여야 한다. + const expectedHash = await sha256Base64Url(sessionToken); + expect(row.idpSessionId).toBe(expectedHash); + // 원문 토큰 자체는 어떤 컬럼에도 저장되지 않는다. + expect(row.idpSessionId).not.toBe(sessionToken); + expect(Object.values(row)).not.toContain(sessionToken); + // 해시 형식(32바이트 base64url = 43자)·원문 토큰 형식 모두 확인. + expect(row.idpSessionId).toMatch(BASE64URL_32BYTES); + expect(sessionToken).toMatch(BASE64URL_32BYTES); + + // 나머지 필드 전파 확인. + expect(row.id).toBe(sessionId); + expect(row.tenantId).toBe("t1"); + expect(row.userId).toBe("u1"); + expect(row.amr).toBe("pwd totp"); // 배열 → 공백 결합 + expect(row.acr).toBe("acr-mfa"); + expect(row.ip).toBe("1.2.3.4"); + expect(row.userAgent).toBe("ua"); + expect(row.lastSeenAt).toBeInstanceOf(Date); + + // 만료 시각 = 생성 시각 + SESSION_TTL_MS (경계 여유 포함). + const exp = (expiresAt as Date).getTime(); + expect(exp).toBeGreaterThanOrEqual(before + SESSION_TTL_MS); + expect(exp).toBeLessThanOrEqual(after + SESSION_TTL_MS); + }); + + it("amr/acr 미지정 시 null 로 저장한다", async () => { + const { db, inserts } = makeDb(); + await createSessionRecord(db, { tenantId: "t1", userId: "u1" }); + expect(inserts[0].amr).toBeNull(); + expect(inserts[0].acr).toBeNull(); + expect(inserts[0].ip).toBeNull(); + expect(inserts[0].userAgent).toBeNull(); + }); +}); + +describe("getSessionContext", () => { + it("유효 세션 라운드트립 — 원문 토큰이 아닌 SHA-256 해시로 조회한다", async () => { + const rowResult = { session: { id: "s1" }, user: { id: "u1" } }; + const { db, selects } = makeDb([rowResult]); + const token = "raw-session-token"; + + const result = await getSessionContext(db, token); + expect(result).toBe(rowResult); + + // 조회 where 의 첫 바인딩은 원문 토큰이 아니라 그 SHA-256 해시여야 한다. + expect(selects.length).toBe(1); + const { sql, params } = render(selects[0].where!); + const expectedHash = await sha256Base64Url(token); + expect(sql).toContain('"sessions"."idp_session_id" = ?'); + expect(params[0]).toBe(expectedHash); + expect(params).not.toContain(token); + }); + + it("만료·폐기·비활성 세션을 DB 레벨 where 가드로 배제한다", async () => { + const { db, selects } = makeDb([]); + await getSessionContext(db, "tok"); + const { sql, params } = render(selects[0].where!); + + // 만료 세션 거부: expires_at > now + expect(sql).toContain('"sessions"."expires_at" > ?'); + // revoke 세션 거부: revoked_at IS NULL + expect(sql).toContain('"sessions"."revoked_at" is null'); + // 비활성 사용자 거부: users.status = 'active' + expect(sql).toContain('"users"."status" = ?'); + expect(params).toContain("active"); + }); + + it("매칭 세션이 없으면 null 을 반환한다", async () => { + const { db } = makeDb([]); + expect(await getSessionContext(db, "tok")).toBeNull(); + }); +}); + +describe("touchSession", () => { + it("lastSeenAt 만 갱신하고 세션 id 로 대상 지정한다", async () => { + const { db, updates } = makeDb(); + const ts = new Date("2026-01-01T00:00:00Z"); + await touchSession(db, "s1", ts); + expect(updates.length).toBe(1); + expect(updates[0].set).toEqual({ lastSeenAt: ts }); + const { sql, params } = render(updates[0].where); + expect(sql).toContain('"sessions"."id" = ?'); + expect(params).toEqual(["s1"]); + }); +}); + +describe("revokeSession", () => { + it("revokedAt 를 설정하고, 아직 폐기되지 않은 대상 세션만 갱신한다", async () => { + const { db, updates } = makeDb(); + const now = new Date("2026-02-02T00:00:00Z"); + await revokeSession(db, "idp-session-id-value", now); + expect(updates.length).toBe(1); + expect(updates[0].set).toEqual({ revokedAt: now }); + + const { sql, params } = render(updates[0].where); + expect(sql).toContain('"sessions"."idp_session_id" = ?'); + // 이미 폐기된 행은 재갱신하지 않는다(멱등). + expect(sql).toContain('"sessions"."revoked_at" is null'); + expect(params[0]).toBe("idp-session-id-value"); + }); +}); + +describe("revokeAllUserSessions", () => { + it("사용자의 모든 미폐기 세션을 revoke 한다(세션 제외 없음)", async () => { + const { db, updates } = makeDb(); + const now = new Date("2026-03-03T00:00:00Z"); + await revokeAllUserSessions(db, "u1", now); + expect(updates[0].set).toEqual({ revokedAt: now }); + + const { sql, params } = render(updates[0].where); + expect(sql).toContain('"sessions"."user_id" = ?'); + expect(sql).toContain('"sessions"."revoked_at" is null'); + // 특정 세션을 남기는 조건(id <>)이 없어야 전부 폐기된다. + expect(sql).not.toContain("<>"); + expect(params).toEqual(["u1"]); + }); +}); + +describe("revokeOtherSessions", () => { + it("keepSessionId 를 제외한 사용자의 미폐기 세션만 revoke 한다", async () => { + const { db, updates } = makeDb(); + const now = new Date("2026-04-04T00:00:00Z"); + await revokeOtherSessions(db, "u1", "keep-1", now); + expect(updates[0].set).toEqual({ revokedAt: now }); + + const { sql, params } = render(updates[0].where); + expect(sql).toContain('"sessions"."user_id" = ?'); + // 유지할 세션(keepSessionId)은 제외. + expect(sql).toContain('"sessions"."id" <> ?'); + // 이미 폐기된 세션은 건드리지 않는다. + expect(sql).toContain('"sessions"."revoked_at" is null'); + expect(params).toEqual(["u1", "keep-1"]); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 91625f9..ab8777f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -22,5 +22,12 @@ export default defineConfig({ test: { environment: "node", include: ["test/**/*.test.ts", "src/**/*.test.ts"], + coverage: { + // @vitest/coverage-v8 기반. 게이트(threshold)는 강제하지 않고 리포트만 산출한다. + provider: "v8", + reporter: ["text", "html"], + reportsDirectory: "./coverage", + include: ["src/lib/**/*.ts"], + }, }, }); From de110d42ba2e958a04039195b7cb7484d0bd6a4b Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 11:14:36 +0900 Subject: [PATCH 07/26] =?UTF-8?q?refactor(admin):=20users/[id]=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EB=B6=84=EB=A6=AC=20+=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EC=97=90=EB=9F=AC=20i18n=20/=20docs:=20=EB=B3=B4?= =?UTF-8?q?=EC=95=88=20=EB=AC=B8=EC=84=9C=20=EC=A0=95=EB=A6=AC=C2=B7?= =?UTF-8?q?=EC=8B=9C=ED=81=AC=EB=A6=BF=20=ED=9A=8C=EC=A0=84=20=EC=A0=88?= =?UTF-8?q?=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - admin/users/[id]/+page.server.ts 677→181줄: user-actions/{profile,org,service,security} 4모듈로 순수 이동(11개 액션 계약·audit kind 보존, forceLogout 신설 포함) - admin 강제 로그아웃 액션: 세션+refresh 폐기, tenant 가드, confirm UI - admin 서버 에러 98곳 i18n 전환(adminError/requireFormId 헬퍼, ko/en 605키 대칭) — UI 라벨 영어·에러 한국어 혼재 해소. 잔존: crud-factory 스키마 문구(테스트 계약)·saml-sps/[id]·프로토콜 API(후속) - 보안 감사 문서 2건 repo 제거(E9), improvement-report 정오표, SECRET_ROTATION.md 신설(7용도·재암호화 3종·무중단 불가 명시), 전수 개선 감사 계획/투두 문서 Co-Authored-By: Claude Fable 5 --- docs/SECRET_ROTATION.md | 48 ++ docs/improvement-report-2026-07-02.md | 9 + docs/plans/deferred-followups/TODO.md | 6 +- docs/plans/project-improvement-audit/PLAN.md | 111 ++++ docs/plans/project-improvement-audit/TODO.md | 194 ++++++ docs/security-audit-2026-05-12.md | 617 ------------------ docs/security-pentest-report.md | 590 ----------------- src/lib/server/admin/errors.ts | 34 + src/lib/server/admin/user-actions/org.ts | 244 +++++++ src/lib/server/admin/user-actions/profile.ts | 142 ++++ src/lib/server/admin/user-actions/security.ts | 41 ++ src/lib/server/admin/user-actions/service.ts | 169 +++++ .../admin/ldap-providers/+page.server.ts | 26 +- src/routes/admin/login/+page.server.ts | 17 +- src/routes/admin/oidc-clients/+page.server.ts | 82 +-- .../admin/oidc-clients/[id]/+page.server.ts | 21 +- src/routes/admin/saml-sps/+page.server.ts | 27 +- src/routes/admin/signing-keys/+page.server.ts | 31 +- src/routes/admin/skins/+page.server.ts | 42 +- src/routes/admin/users/+page.server.ts | 58 +- src/routes/admin/users/[id]/+page.server.ts | 538 +-------------- src/routes/admin/users/[id]/+page.svelte | 18 + 22 files changed, 1212 insertions(+), 1853 deletions(-) create mode 100644 docs/SECRET_ROTATION.md create mode 100644 docs/plans/project-improvement-audit/PLAN.md create mode 100644 docs/plans/project-improvement-audit/TODO.md delete mode 100644 docs/security-audit-2026-05-12.md delete mode 100644 docs/security-pentest-report.md create mode 100644 src/lib/server/admin/errors.ts create mode 100644 src/lib/server/admin/user-actions/org.ts create mode 100644 src/lib/server/admin/user-actions/profile.ts create mode 100644 src/lib/server/admin/user-actions/security.ts create mode 100644 src/lib/server/admin/user-actions/service.ts diff --git a/docs/SECRET_ROTATION.md b/docs/SECRET_ROTATION.md new file mode 100644 index 0000000..7d3ac96 --- /dev/null +++ b/docs/SECRET_ROTATION.md @@ -0,0 +1,48 @@ +# `IDP_SIGNING_KEY_SECRET` 회전 절차 + +> 작성: 2026-07-06 (코드 기준 조사: `src/lib/server/crypto/keys.ts`, `auth/totp.ts`, `auth/mfa.ts`, `auth/webauthn.ts`, `audit/index.ts`). +> 이 시크릿은 단일 마스터 값이며 **키 버전/이중 시크릿 fallback이 없다** — 무중단(zero-downtime) 회전은 현재 코드로는 불가능하고, 아래 절차의 재암호화 창 동안 일부 요청이 실패할 수 있다. + +## 1. 이 시크릿이 쓰이는 곳 (7용도) + +| # | 용도 | 방식 | 저장 데이터 | 근거 | +| --- | ------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------ | +| 1 | OIDC/SAML 서명용 RSA private JWK 래핑 | HKDF(`idp-signing-key-wrap-v1`) → AES-256-GCM | `signing_keys.private_jwk_encrypted` (활성 행만 런타임 사용) | `crypto/keys.ts:37-69` | +| 2 | Opaque access token 서명/검증 | **원문 그대로** HMAC-SHA256 키 | 저장 없음 (TTL 300초) | `crypto/keys.ts:213-250` | +| 3 | LDAP admin bind password 암호화 | HKDF(`idp-ldap-bind-password-v1`) → AES-256-GCM | `identity_providers.config_json` 내 `bindPasswordEnc` | `crypto/keys.ts:258-280` | +| 4 | TOTP seed 암호화 | HKDF(v2: `idp-totp-secret-wrap-v2:`, v1 레거시) → AES-256-GCM | `credentials.secret` (type='totp') | `auth/totp.ts:99-175` | +| 5 | MFA pending 쿠키 서명 | 원문 HMAC | 저장 없음 (쿠키, TTL 5분) | `auth/mfa.ts:44-92` | +| 6 | WebAuthn challenge 쿠키 서명 | 원문 HMAC | 저장 없음 (쿠키, TTL 5분) | `auth/webauthn.ts:28-58` | +| 7 | 감사 로그 행 무결성 MAC | 원문 HMAC | `audit_events.hash` (평문 필드에 대한 MAC — 암호화 아님) | `audit/index.ts:93-124` | + +## 2. 회전 시 영향 분류 + +**A. 자연 해소(조치 불필요)** — 발급된 access token(최대 5분 내 만료), MFA pending·WebAuthn challenge 쿠키(5분, 재시도로 해소). `oidc_refresh_tokens`는 이 시크릿과 무관(랜덤 토큰 + SHA-256 해시)이라 영향 없음. + +**B. 재암호화 필수(누락 시 장애)** — 아래 3종은 old secret으로 복호해 new secret으로 재암호화하지 않으면 **영구 복호 불가**: + +| 데이터 | 미조치 시 증상 | +| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| `signing_keys` 활성 행(`active=true AND rotated_at IS NULL`)의 `private_jwk_encrypted` | unwrap 예외(try/catch 없음) → OIDC 토큰 발급·SAML SSO 전면 500 | +| `credentials.secret` (TOTP 전량) | `api/totp/verify` 500 → TOTP 사용자 전원 MFA 로그인 불가 | +| `identity_providers.config_json.bindPasswordEnc` (LDAP) | 복호 실패는 try/catch로 흡수되나 bind 자격증명 부재 → 해당 LDAP 로그인 전부 실패 | + +**C. 선택 후처리** — `audit_events.hash`: 원본 평문이 DB에 있으므로 new secret으로 전량 재계산 가능(전용 스크립트는 현재 없음). 재계산하지 않으면 향후 무결성 검증 시 old-secret 시절 행이 전부 오탐된다. + +## 3. 회전 절차 (순서 엄수) + +1. **준비**: 새 secret 생성(고엔트로피 256bit 이상). old secret을 재암호화 작업 완료까지 안전하게 보관. 트래픽이 낮은 시간대 선택(재암호화~재배포 사이 실패 창 존재). +2. **재암호화 배치 실행** (전용 스크립트 신규 작성 필요 — 리포에 기성 스크립트 없음): + - 각 테넌트 `signing_keys` 활성 행: `unwrapPrivateKey(old)` → `wrapPrivateKey(new)` 재래핑. (`rotated_at`이 찍힌 과거 행은 코드가 읽지 않으므로 스킵 가능.) + - `credentials` type='totp' 전량: `decryptTotpSecret(old, userId)` → `encryptTotpSecret(new, userId)` (v1 레거시 행은 userId 바인딩 없이 복호됨). + - LDAP `bindPasswordEnc`: `decryptSecret(old, "idp-ldap-bind-password-v1")` → `encryptSecret(new, ...)`. +3. **시크릿 교체·재배포**: `wrangler secret put IDP_SIGNING_KEY_SECRET`(Workers) 또는 환경변수 교체(Node) 후 재배포. **반드시 2단계 완료 후에.** +4. **스모크 테스트**: `/oidc/token` 발급, `/saml/sso` 서명, TOTP 로그인, LDAP 로그인 — 전부 무보호 복호 경로를 타므로 실패 시 즉시 드러난다. JWKS(`/oidc/jwks`)의 kid 정상 노출 확인. +5. **(선택)** `audit_events.hash` 전량 재계산 배치. +6. **old secret 파기**. + +## 4. 알려진 한계 / 개선 여지 (별도 트랙) + +- 다중 시크릿 fallback(old/new 동시 시도)이 없어 완전 무중단 회전 불가 — 시크릿 버전 태깅 + 순차 재암호화 지원이 근본 해법. +- access-token HMAC·쿠키 서명(#2,#5,#6,#7)이 파생 없이 원문을 공유 — HKDF 도메인 분리로 통일하면 용도별 노출 반경이 줄어든다(단, 교체 시 위 A 항목의 즉시 무효화 특성은 동일). +- admin 콘솔의 "서명키 rotate" 액션은 **현재 secret으로 새 서명키를 만드는 것**이지 이 마스터 시크릿 회전과 무관하다 — 혼동 주의. diff --git a/docs/improvement-report-2026-07-02.md b/docs/improvement-report-2026-07-02.md index 3a6fbf8..8b15b01 100644 --- a/docs/improvement-report-2026-07-02.md +++ b/docs/improvement-report-2026-07-02.md @@ -3,6 +3,15 @@ > 3개 병렬 분석(미구현·갭 전수 탐색 / 보안 취약점 리뷰 / 아키텍처·품질 리뷰) 결과를 종합. > 분석 기준 브랜치: `chore/remove-d1-binding` (origin/main과 동일, 미커밋 변경 없음). +> **⚠️ 정오표 (2026-07-06 추가)** — 이 리포트의 일부 서술은 이후 커밋으로 stale 해졌다. 최신 현황은 `docs/plans/project-improvement-audit/PLAN.md` 참조. +> +> - "Refresh Token 미구현"(A1) → **구현 완료** (`oidc/token`의 refresh_token grant, 회전+재사용 감지 family 폐기 포함). +> - "SAML Assertion 암호화 미구현" → **구현 완료** (`test/unit/saml-encrypt.test.ts`, `verify:saml-encryption` 스크립트 존재). +> - "TOTP enroll TOCTOU 미해소" → **해소** (deferred-followups Phase A — `totp_owner_id` unique index + INSERT 409 처리). +> - "테스트 프레임워크·CI 테스트 단계 전무" → vitest 62+ 테스트 및 CI 게이트(lint/check/test/build/CodeQL/gitleaks) 구축됨. +> - groups/address/organization scope "보류" → **구현 완료** (claims.ts, token/userinfo 매핑). +> - E9(보안 감사 문서 public repo 잔존) → **처리 완료** (2026-07-06, repo에서 제거 — 히스토리에는 잔존). + ## 종합 판정 기존 보안 감사(2026-05-12)의 Critical/High 대부분이 실제 코드에 반영되어 있고, **신규 Critical/High 취약점은 없다**. 현재 프로젝트의 부채는 세 갈래로 요약된다. diff --git a/docs/plans/deferred-followups/TODO.md b/docs/plans/deferred-followups/TODO.md index 0d6d97f..8f1595f 100644 --- a/docs/plans/deferred-followups/TODO.md +++ b/docs/plans/deferred-followups/TODO.md @@ -1,8 +1,8 @@ # 실행 투두 — 보류 3건 (병렬 A/B/C) -> **상태(2026-07-05): A/B/C 전부 구현 완료 → 메인 브랜치 통합 완료 → 독립 검증 통과.** -> 게이트: `bun run test` 54 passed(9 files) · `svelte-check` 0 errors · `eslint`·`build` 통과. -> 마이그레이션 생성만(d1 0019, pg/mysql/sqlite 0002) — **적용은 사용자 몫**. 커밋 승인 대기. +> **상태(2026-07-06 갱신): A/B/C 전부 구현·검증·커밋 완료(병합됨).** 이후 admin 콘솔 영어화(cc44d7c)·auth 서버 에러 i18n(1c8c5c0)으로 당초 "Out" 범위였던 항목도 추가 반영됨. +> 마이그레이션 생성분(d1 0019, pg/mysql/sqlite 0002)의 **실제 DB 적용 여부는 사용자 확인 필요**(미적용이면 배포 전 적용 요망). +> 후속 전수 개선은 `docs/plans/project-improvement-audit/` 참조. > 승인된 결정: 기획서 승인 · Phase C FK 검증 **추가** · Phase B 자동판별+**수동 토글** · **병렬 진행**(worktree 격리). > 규칙: 스키마 변경은 `db:generate` 까지만(적용 금지). 커밋은 검증 통과 + 승인 후. 쓰기=Opus 위임, 검증=작성자와 분리된 독립 Opus 에이전트. diff --git a/docs/plans/project-improvement-audit/PLAN.md b/docs/plans/project-improvement-audit/PLAN.md new file mode 100644 index 0000000..375f5d0 --- /dev/null +++ b/docs/plans/project-improvement-audit/PLAN.md @@ -0,0 +1,111 @@ +# 프로젝트 전수 개선 감사 기획서 — KeyStone + +> 요청: "이 프로젝트 내에서 개선하거나 수정할 내용들, 추가할 내용들을 모두 파악해서 적용될 수 있도록" +> 조사: 2026-07-05~06, 5개 병렬 Explore 에이전트(보안=Opus, 아키텍처·DB=Opus, 품질/테스트·CI/기능·UX=Sonnet). +> 전제: Critical/High 보안 취약점 없음. 이전 리포트(2026-07-02)·deferred-followups(TOTP TOCTOU/i18n/CRUD 팩토리)는 모두 구현·커밋 완료 확인. + +--- + +## 배경 / 목표 + +프로젝트는 문서·CI·보안 방어(세션/CSRF/OIDC/SAML/LDAP) 수준이 이미 높으나, 조사 결과 다음 5축의 격차가 확인됨: + +1. **보안 Medium 6건** — 타이밍 열거, 계정 잠금 부재, tenant 격리 우회 1건, code 교환 세션 검사 누락 등. +2. **운영 시한폭탄** — 만료 데이터 GC 전무(purge 함수 3개가 orphan, cron 없음 → 9종 테이블 무한 성장), 마이그레이션 4트랙 비대칭(d1 21개 vs 나머지 4개). +3. **기능 공백** — 이메일 인증 플로우 부재로 `email_verified` 클레임이 **항상 false**(RP 신뢰 시 실질 버그), 보안 알림 메일 부재. +4. **테스트 불균형** — 62개 유닛 테스트가 전부 그린이지만 정작 **토큰 서명/발급·세션·패스워드·rate-limit 알고리즘은 테스트 0건**. schema-parity가 인덱스/제약 drift를 못 잡음. +5. **품질·문서 부채** — `users/[id]` 677줄, admin "라벨은 영어·에러는 한국어" 혼재, **보안 감사 문서 2건 public repo 잔존(E9 미처리)**, 문서·메모리 drift. + +목표: 위 격차를 페이즈별로 해소해 적용 가능한 상태로 만든다. 스키마 변경은 `db:generate`까지만(적용은 사용자). + +--- + +## 범위 (In / Out) + +### Phase 1 — 보안 Medium 일괄 해소 + +- **In**: + - **S1 로그인 타이밍 열거**: 미존재 계정에도 더미 scrypt 검증 수행해 응답시간 균등화 (`src/lib/server/auth/users.ts:46-63`; find-id/find-password의 기존 균등화 패턴 재사용). + - **S2 계정 단위 잠금**: 기존 rate-limit 인프라를 `login:user:` 키로 재사용해 계정별 backoff 추가 — **스키마 변경 없이** 구현 (IP rate-limit과 병행). + - **S3 lookup API tenant 격리**: `api/users/lookup/+server.ts:35-38`의 `?id=` 경로에 tenant 스코프 강제(또는 호출자 tenant 명시 요구). + - **S4 code 교환 세션 검사**: authorization_code 교환 시 `grant.sessionId` 세션의 `revokedAt`/만료 확인 (`oidc/token/+server.ts:392-438`; refresh 경로의 기존 검사 로직 재사용). + - **S5 issuer fail-closed**: 프로덕션에서 `IDP_ISSUER_URL` 미설정 시 host-header fallback 대신 명시적 실패(503) + 부트스트랩 시 필수 env(`IDP_SIGNING_KEY_SECRET` 포함) 검증(fail-fast). +- **Out**: 마스터 시크릿 용도 분리·회전 절차(운영 하드닝 트랙, 별도 작업 — 회전 절차 문서화만 Phase 5에 포함), skin sanitize 강화(Low, CSP로 완화 중), WebAuthn rpID(Low, 실질 위조 불가). + +### Phase 2 — 운영 안정성 (GC·마이그레이션·원자성·캐시) + +- **In**: + - **O1 만료 데이터 GC**: Cloudflare Cron Trigger + `scheduled()` 핸들러 신설, 만료 대상 전 테이블(sessions/oidc_grants/refresh_tokens/password_reset_tokens/webauthn challenges/rate_limits/saml_slo_states/saml_authn_request_ids/saml_sessions) 일괄 purge. Node 런타임 대응 경로 포함. 기존 orphan purge 함수 3개 재사용·확장. wrangler.example.jsonc에 cron 설정 추가. + - **O2 마이그레이션 트랙 대칭화**: `db:generate:all` 스크립트 추가, 4트랙 미생성 diff를 잡는 CI 체크(또는 parity 테스트), pg/mysql/sqlite 트랙의 밀린 마이그레이션 생성(**적용은 안 함**). + - **O3 refresh 회전 원자화**: claim→insert 2-write를 signing-keys/totp-confirm과 동일한 batch/transaction 분기로 원자화. 이 분기 패턴을 `atomic()` 공용 유틸로 추출(3곳 재사용). + - **O4 서명키 캐시**: `getActiveSigningKey`/`getPublicJwks`에 baseline과 동일한 globalThis 단기 캐시(회전 시 무효화). + - **O5 스키마 정리**: `client_skins.createdAt` sqlite 단위 통일(timestamp_ms), legacy `oidc_grants.code` 컬럼+인덱스 drop — `db:generate`까지만. `drizzle.config.ts:9`의 사실과 다른 gitignore 주석 정리. +- **Out**: **rate-limit 저장소 DO/KV 추상화**(H3 — 구조 변경이 커서 별도 트랙 권장, 열린 질문 ③), mysql signing_keys 부트스트랩 경로 FOR UPDATE(Low, 발생 조건 희귀). + +### Phase 3 — 기능 추가 (이메일 인증 + 보안 알림) + +- **In**: + - **F1 이메일 인증 플로우**: 가입 시 인증 메일 발송 → 토큰 검증 라우트 → `users.emailVerifiedAt` 세팅 → `email_verified` 클레임 정상화. 재발송·만료 처리 포함. (토큰 테이블은 기존 password_reset_tokens 패턴 재사용, 스키마 필요 시 `db:generate`까지만.) + - **F2 보안 알림 메일**: 비밀번호 변경/재설정 완료, MFA 등록·삭제, 패스키 등록·삭제, admin의 비밀번호 초기화·계정 잠금 시 대상 사용자에게 통지 메일 (기존 email.ts 템플릿 패턴 확장). + - **F3 admin 강제 로그아웃 액션**: `revokeAllUserSessions` 재사용한 전용 버튼/액션 (소규모). +- **Out(별도 후속 후보)**: 초대(invite) 플로우, 셀프서비스 활성 세션 목록/철회, 계정 삭제 — 각각 독립 기획 필요(열린 질문 ①). 새 기기/위치 로그인 알림(기기 지문 인프라 필요). + +### Phase 4 — 테스트 확충 (P0/P1) + +- **In**: + - **T1 P0 유닛 테스트**: `crypto/keys.ts`(JWT 서명·access token HMAC·키 래핑), `oidc/grant.ts`, `oidc/refresh.ts`(회전·재사용 감지), `auth/session.ts`, `auth/password.ts`(scrypt·레거시 업그레이드), `ratelimit/index.ts`(슬라이딩 윈도우 본체). Phase 1~3 신규 로직(계정 잠금, 이메일 인증 토큰, GC purge) 테스트 포함. + - **T2 schema-parity 강화**: 인덱스/unique 제약 parity 비교 추가(방언별 API 차이는 명시적 예외 목록으로 — mysql partial-unique 등 known gap을 테스트가 문서화), 컬럼 타입·nullable 비교. + - **T3 인프라**: `test:coverage`(@vitest/coverage-v8) + `typecheck`·`lint:fix`·`db:check` 스크립트, lefthook pre-commit에 eslint 추가. +- **Out**: 통합/e2e 하네스(Playwright 등 — 가치 크지만 별도 기획 규모), LDAP/SAML response 유닛(P2, 후속). + +### Phase 5 — 품질·문서 정리 + +- **In**: + - **Q1 보안 문서 격리**: `docs/security-audit-2026-05-12.md`·`docs/security-pentest-report.md` repo에서 제거(E9) — 처리 방식은 열린 질문 ②. + - **Q2 `users/[id]/+page.server.ts`(677줄) 분리**: actions를 도메인별 모듈(profile/org/service)로 분리, 동작 무변경. + - **Q3 admin 에러 메시지 i18n**: 비정형 admin 라우트의 하드코딩 한국어 에러를 t()/translate() 전환(UI 라벨은 이미 영어 — 혼재 해소). 공통 `requireFormId` 류 헬퍼 추출로 중복 문구 12곳+ 정리. + - **Q4 문서·메모리 정합**: improvement-report의 stale 항목(refresh token "미구현" 표기 등) 갱신, deferred-followups TODO 상태 갱신, 프로젝트 메모리(OIDC scope 후속) 최신화, 시크릿 회전 절차 문서화. +- **Out**: saml/sso·oidc/token 파일 물리 분리(Low, 함수 단위로는 이미 정리됨), knip/depcheck 도입(Low), en.json 원어민 검수(외부 작업). + +--- + +## 코드베이스 사실관계 (조사 근거 요약) + +- **보안 양호 확인**: 세션(해시 저장·회전·cascade 폐기), CSRF(전역 Origin 강제+double-submit), OIDC(PKCE S256 전용, redirect_uri 정확일치, code 1회 소진, refresh 회전+family 폐기), SAML(XSW 다층 방어, ACS 강제), LDAP(필터/DN 이스케이프), scrypt(N=2^15,r=8,p=3)+레거시 자동 업그레이드, hash 기반 CSP. +- **보안 결함 근거**: S1=`users.ts:46-63`(미존재 시 해시 미수행), S2=`login/+page.server.ts:120`(IP rate-limit뿐), S3=`api/users/lookup/+server.ts:35-38`(id 경로 tenant 무필터), S4=`oidc/token/+server.ts:392-438`(code 경로만 세션 미검사), S5=`runtime.ts:55-62`(Host fallback)+`bootstrap.ts:106`(시크릿 미설정 시 조용히 스킵). +- **GC 부재**: `webauthn.ts:158`·`refresh.ts:178`·`ratelimit/index.ts:84`의 purge 함수 호출부 0건, wrangler cron 전무. sessions/oidc*grants/password_reset_tokens/saml*\* 은 purge 함수조차 없음. rate_limits는 요청마다 신규 key 생성으로 폭증. +- **마이그레이션 비대칭**: d1 트랙 21개(0000–0020) vs pg/mysql/sqlite 각 4개. d1·sqlite가 같은 스키마 공유하는데 히스토리 상이. `drizzle.config.ts:9` 주석은 gitignore라 하나 실제 tracked. +- **테스트 현황**: 10파일 62테스트 전부 순수 로직. `+server.ts` 23 + `+page.server.ts` 27개 라우트 핸들러 직접 구동 테스트 0. CI(lint/check/test/build/CodeQL/gitleaks/dependabot)는 양호. +- **기능 공백 근거**: `emailVerifiedAt` 세팅 코드 0건(읽기 전용)인데 `token/+server.ts:86`·`userinfo/+server.ts:73`이 클레임으로 노출. email.ts 템플릿 2종(find-id/reset)뿐. +- **이미 구현 확인(문서만 stale)**: OIDC groups/address/organization scope, refresh_token grant, introspect/revoke, 감사 해시, TOTP TOCTOU, CRUD 팩토리+zod+FK 검증, i18n en 476키 1:1 대칭+LocaleToggle+SSR 로케일. SAML assertion 암호화도 구현된 것으로 판단(saml-encrypt.test.ts 라운드트립 + verify:saml-encryption 스크립트 존재; 기능 조사 레인의 "미구현" 보고는 오판으로 보이며 실행 시 재확인). + +--- + +## 접근법 / 리스크 / 트레이드오프 + +| Phase | 접근 | 리스크 & 완화 | +| -------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| 1 보안 | 기존 패턴 재사용(타이밍 균등화·rate-limit 키·refresh의 세션 검사)으로 스키마 무변경 최소 수정 | 인증 핫패스 수정 → Phase 4 테스트가 커버 + 독립 검증 레인. S2는 정상 사용자 오탐 위험 → 창/임계값 보수적(예: 10회/15분 backoff) | +| 2 운영 | cron+scheduled 신설, 나머지는 기존 유틸 패턴 추출·재사용 | O2 마이그레이션 생성물은 커밋만, 적용은 사용자(CLAUDE.md 규칙). GC DELETE는 만료분만·배치 크기 제한으로 타임아웃 방지 | +| 3 기능 | password_reset_tokens 검증된 패턴 복제로 이메일 인증 구현 | 기존 가입 사용자 처리(전원 미인증 상태) → 로그인 차단은 하지 않고 클레임·배너만(비파괴적) | +| 4 테스트 | WebCrypto/HMAC은 실제 키로, DB는 기존 mock 패턴 | 없음(추가만) | +| 5 품질 | 동작 무변경 리팩터 + 문서 갱신 | Q2는 diff 리뷰로 회귀 방지, Q1은 히스토리에 문서가 남는 한계 명시 | + +**의존성**: Phase 1·2·3은 파일 겹침 적어 부분 병렬 가능(단 S5와 O1이 hooks/bootstrap 근처에서 겹칠 수 있어 순서 조율). Phase 4는 1~3 이후(신규 로직 테스트 포함). Phase 5는 독립. + +--- + +## 열린 질문 (승인 시 확정) + +1. **Phase 3 범위** — 이메일 인증+보안 알림+강제 로그아웃(제안)만? 아니면 초대 플로우/세션 셀프서비스까지 이번에 포함? (후자는 규모가 커져 별도 기획 권장) +2. **보안 감사 문서 2건(E9)** — (a) repo에서 삭제만(히스토리 잔존 감수, 권장), (b) private 위치로 이동 후 삭제, (c) 유지. ※ 히스토리 rewrite는 파괴적이라 제안하지 않음. +3. **rate-limit 저장소 추상화(DO/KV)** — 이번 범위에 포함할지, 별도 트랙으로 미룰지(권장: 미룸 — 구조 변경이 크고 GC(O1)로 폭증 문제는 완화됨). +4. **진행 방식** — 5페이즈 전체 승인 후 순차(1→2→3→4→5) 진행 권장. 일부 페이즈만 선별 진행도 가능. + +--- + +## 산출물 + +- `docs/plans/project-improvement-audit/PLAN.md` (본 문서) +- 승인 후 `docs/plans/project-improvement-audit/TODO.md` (페이즈별 상세 투두 + 수용 기준) +- 스키마 변경분은 `db:generate` 마이그레이션 생성까지만 — 적용은 사용자 요청. diff --git a/docs/plans/project-improvement-audit/TODO.md b/docs/plans/project-improvement-audit/TODO.md new file mode 100644 index 0000000..df01e5a --- /dev/null +++ b/docs/plans/project-improvement-audit/TODO.md @@ -0,0 +1,194 @@ +# 실행 투두 — 프로젝트 전수 개선 (5 페이즈) + +> 승인된 결정(2026-07-06, 게이트 ①): 기획서 승인 · Phase 3 = **제안 범위**(이메일 인증 + 보안 알림 + 강제 로그아웃, 초대/세션 셀프서비스는 후속) · 보안 감사 문서 2건 = **git rm 삭제**(히스토리 rewrite 없음) · rate-limit 저장소 추상화 = **별도 트랙으로 미룸**. +> 규칙: 스키마 변경은 `db:generate`까지만(적용 금지, CLAUDE.md). 커밋은 전체 검증 통과 + 커밋 승인(게이트 이후)에만, 푸시 금지. 쓰기=Opus 위임, 검증=작성자와 분리된 독립 Opus 에이전트. 스텁/TODO 플레이스홀더/`test.skip`은 블로커. +> 순서: Phase 1 → 2 → 3 → 4 → 5 (4는 1~3의 신규 로직 테스트 포함, 5는 독립이라 병렬 가능). 페이즈 내 파일 겹침 태스크는 직렬. + +--- + +## Phase 1 — 보안 Medium 5건 수정 + +**목적**: 조사에서 확인된 Medium 결함을 스키마 변경 없이 기존 패턴 재사용으로 해소. +**파일 경계**: S1·S2는 login 경로 공유 → 같은 에이전트가 직렬 처리. S3·S4·S5는 서로 독립. + +### [x] 1-1. S1 로그인 타이밍 열거 균등화 + +- 파일: `src/lib/server/auth/users.ts` (46-63 부근), 참고: `find-password/+page.server.ts`의 timing 균등화 패턴 +- 작업: username 미존재 시에도 더미 scrypt 검증(고정 더미 해시에 대한 verify)을 수행해 존재/미존재 응답시간을 균등화. 더미 해시는 실제 파라미터(N=2^15,r=8,p=3)와 동일 비용. +- 수용 기준: 미존재/존재-오답 경로 모두 scrypt 1회 비용 발생. 기존 로그인 성공/실패 동작·에러 메시지 무변경. 레거시 해시(argon2id/PBKDF2) 업그레이드 경로 회귀 없음. + +### [x] 1-2. S2 계정 단위 잠금(backoff) + +- 파일: `src/routes/(auth)/login/+page.server.ts` (120 부근), `src/lib/server/ratelimit/` 재사용 +- 작업: 기존 rate-limit 인프라를 `login:user:` 키로 추가 적용(스키마 무변경). 보수적 임계값(예: 10회 실패/15분)으로 IP 제한과 병행. 실패 시에만 카운트(성공 로그인은 미카운트). 잠금 시 에러 메시지는 계정 존재를 새로 노출하지 않는 문구(기존 generic 실패 문구 계열, i18n ko/en 키 추가). +- 수용 기준: 분산 IP에서도 동일 계정 연속 실패가 제한됨. 정상 사용자 오탐 최소화(임계값·창 명시). MFA/LDAP 로그인 경로와 충돌 없음. i18n 키 ko/en 대칭. + +### [x] 1-3. S3 lookup API tenant 격리 + +- 파일: `src/routes/api/users/lookup/+server.ts` (35-38) +- 작업: `?id=` 경로에도 tenant 스코프 강제. 기존 호출자(dispatcher)의 사용 방식을 먼저 읽기 위임으로 확인 후, tenant 명시 파라미터 요구 또는 요청 컨텍스트의 tenant로 필터 — 호환성 깨지지 않는 쪽 선택. +- 수용 기준: 타 tenant 사용자 id 조회가 차단됨. 기존 정상 호출 시나리오 무회귀(주석의 "tenant 무관" 의도가 실제 요구사항이었는지 확인 근거 포함). + +### [x] 1-4. S4 authorization_code 교환 시 세션 폐기 검사 + +- 파일: `src/routes/oidc/token/+server.ts` (392-438), 참고: 같은 파일 refresh 경로(305-318)의 세션 검사 +- 작업: code 교환 시 `grant.sessionId` 세션의 `revokedAt`/만료를 확인, 폐기된 세션이면 `invalid_grant` 반환. refresh 경로 검사 로직을 헬퍼로 추출해 양쪽 재사용. +- 수용 기준: 로그아웃된 세션의 미소진 code가 교환 거부됨. 정상 플로우(authorize→token) 무회귀. 세션 없는 grant(있다면 service 계열)의 기존 동작 보존. + +### [x] 1-5. S5 issuer fail-closed + 필수 env 검증 + +- 파일: `src/lib/server/auth/runtime.ts` (55-62), `src/lib/server/auth/bootstrap.ts` (106 부근) +- 작업: 프로덕션(`import.meta.env.PROD` 또는 동등 판별)에서 `IDP_ISSUER_URL` 미설정 시 host fallback 대신 명시적 오류(503). `IDP_SIGNING_KEY_SECRET` 미설정 시 조용한 스킵 대신 명확한 오류/경고 집계(부트스트랩 1회). dev 모드는 기존 fallback 유지(로컬 DX 보존). `.env.example`·README 해당 항목에 "프로덕션 필수" 명시. +- 수용 기준: 프로덕션 미설정 시 토큰 발급 시점이 아니라 요청 초기에 명확한 오류. dev 로컬 플로우 무회귀. 문서 갱신 포함. + +### [x] 1-검증 (독립 Opus 에이전트) — 통과(2026-07-06): 5건 전부 VERIFIED, 게이트 4종 그린(README prettier·wrangler types 스테일은 기계적 정리 완료). 참고: 잠금 실효 임계 11회(peek 1회 지연, 코드 주석화), argon2id 레거시 계정 잔존 타이밍 편차는 마이그레이션 창 고유 한계. + +- 수용 기준 5건 각각 통과/실패 판정(파일:라인 근거). `bun run test`·`bun run check`·`bun run lint`·`bun run build` 그린. 타이밍 균등화가 실제로 동일 비용 경로인지, 계정 잠금이 사용자 열거 신규 오라클을 만들지 않는지 중점 리뷰. 가짜 완료(스텁/skip) 스캔. + +--- + +## Phase 2 — 운영 안정성 (GC·마이그레이션·원자성·캐시·스키마 정리) + +**목적**: 무한 성장 테이블 GC, 4트랙 마이그레이션 대칭화, 비원자 write 해소, 핫패스 캐시. +**파일 경계**: 2-1(GC)과 2-3(원자화)이 `refresh.ts`를 공유 → 직렬. 2-2/2-4/2-5는 독립. + +### [x] 2-1. O1 만료 데이터 GC — Workers는 adapter 제약으로 cron 대신 확률적 waitUntil GC(1%), Node는 setInterval. sessions 유예 30일(refresh 우회 방지 근거 검증됨) + +- 파일: `src/worker.ts`(또는 Workers 엔트리 — 실제 엔트리 파일은 읽기 위임으로 확인), `src/lib/server/db/gc.ts`(신규), `wrangler.example.jsonc`, 기존 `webauthn.ts:158`·`refresh.ts:178`·`ratelimit/index.ts:84` purge 함수 +- 작업: (a) `gc.ts`에 만료 대상 전 테이블 purge 통합 함수 — sessions(만료+revoked 유예 경과), oidc_grants(만료/소진), oidc_refresh_tokens, password_reset_tokens, webauthn_challenges, rate_limits(경과 윈도우), saml_slo_states, saml_authn_request_ids, saml_sessions. 배치 크기 제한(예: 테이블당 LIMIT n 반복 또는 1패스)으로 타임아웃 방지. (b) Workers: `triggers.crons`(예: 1시간 간격) + `scheduled()` 핸들러. (c) Node: 서버 기동 시 setInterval(중복 기동 가드). (d) 기존 orphan purge 3함수는 gc.ts로 통합하거나 호출 연결. +- 수용 기준: 두 런타임 모두에서 GC 실행 경로 존재. 만료되지 않은 데이터는 절대 삭제 안 됨(각 테이블 조건 명시). wrangler.example.jsonc에 cron 문서화. 삭제 실패가 요청 처리에 영향 없음(격리된 에러 처리). + +### [x] 2-2. O2 마이그레이션 트랙 대칭화 — db:generate:all + CI 드리프트 체크 + d1 offline generate 침묵 버그(d1-http driver) 수정 + +- 파일: `package.json`(scripts), `drizzle/**`(4트랙), `test/unit/`(신규 parity 체크) 또는 `.github/workflows/ci.yml` +- 작업: (a) `db:generate:all` 스크립트(4방언 순차 실행). (b) pg/mysql/sqlite 트랙의 밀린 마이그레이션을 `db:generate:{pg,mysql,sqlite}`로 생성 — **생성만, 적용 금지**. (c) 미생성 diff 감지 장치: CI 스텝(4방언 generate 후 git diff 검사) 또는 스냅샷 기반 테스트 중 실행 가능한 쪽. (d) `drizzle.config.ts:9`의 사실과 다른 ".gitignore 대상" 주석 제거(트랙 커밋 정책 확정 반영). +- 수용 기준: 4트랙 모두 현 스키마와 스냅샷 일치(추가 generate 시 no-op). 이후 스키마 변경 시 한 트랙만 갱신하면 CI/테스트가 실패하는 안전망 존재. 마이그레이션 SQL은 커밋 대상으로 보고만. + +### [x] 2-3. O3 refresh 회전 원자화 + atomic 유틸 — 무조건 insert+원자 단위 설계, 패자 stray는 family 폐기로 무효화(검증 증명됨) + +- 파일: `src/lib/server/db/atomic.ts`(신규), `src/lib/server/oidc/refresh.ts` (147 부근), `src/routes/admin/signing-keys/+page.server.ts` (86-93), `src/routes/api/totp/enroll/confirm/+server.ts` (93-100) +- 작업: d1/sqlite=batch, pg/mysql=transaction 분기를 `atomic()` 공용 유틸로 추출. refresh 회전의 claim(UPDATE)→insert 2-write를 원자화(단, 기존 "원자적 claim으로 동시 회전 경쟁 판정" 시맨틱 보존 — claim 결과 확인 후 insert가 같은 원자 단위에 들어가도록 설계 검토, batch 제약상 불가하면 claim은 유지하고 insert 실패 시 claim 롤백/복구 경로라도 확보). 기존 2곳 복붙 분기를 유틸로 치환. +- 수용 기준: claim 성공·insert 실패로 old-revoked+new-부재가 되는 창이 제거(또는 복구 가능). 재사용 감지(family 폐기) 시맨틱 무회귀. 기존 signing-keys/totp-confirm 동작 무변경. 3곳 모두 유틸 사용. + +### [x] 2-4. O4 서명키/JWKS 캐시 — tenant별 60초 TTL, 회전 시 무효화, null 미캐시 + +- 파일: `src/lib/server/crypto/keys.ts`, 참고: `bootstrap.ts:94-110`의 globalThis 캐시 패턴 +- 작업: `getActiveSigningKey`/`getPublicJwks`에 tenant별 globalThis 단기 캐시(TTL 예: 60~300초). admin 서명키 회전/비활성화 액션에서 캐시 무효화 호출. +- 수용 기준: 토큰 발급·JWKS 요청이 캐시 히트 시 DB 미조회. 키 회전 후 TTL 내 무효화로 새 키 반영. 멀티테넌트 키 혼선 없음(캐시 키에 tenant 포함). + +### [x] 2-5. O5 스키마 정리 (`db:generate`까지만) — legacy code drop + client_skins ×1000 보정(가드 포함), 4트랙 생성·no-op 확인, 적용은 사용자 몫 + +- 파일: `src/lib/server/db/schema.{sqlite,pg,mysql}.ts`, `drizzle/**`(생성물) +- 작업: (a) `client_skins.createdAt` sqlite `timestamp` → `timestamp_ms` 통일. (b) legacy `oidc_grants.code` 컬럼 + `oidc_grants_code_uidx` 3방언 drop(잔존 평문 code 데이터가 있어도 TTL 5분이라 안전 — 마이그레이션 주석에 명시). (c) 4트랙 `db:generate:all`로 마이그레이션 생성. **적용 명령 실행 금지.** +- 수용 기준: schema-parity 테스트 통과. 4트랙 마이그레이션 생성됨. codeHash 경로만 남고 `code` 참조 코드 0건(사전 grep 확인). 적용은 사용자 요청으로 보고. + +### [x] 2-검증 (독립 Opus 에이전트) — 통과(2026-07-06): A1~D5 전부 VERIFIED, 게이트 4종 그린(refresh.ts prettier만 정리). 잔여 권고: gc.ts SESSION_GC_GRACE_MS 주석 근거를 revoke-on-logout 불변식 기준으로 정밀화(Phase 4-2 에이전트에 위임) + +- GC 삭제 조건의 안전성(미만료 데이터 삭제 불가) 집중 리뷰, 마이그레이션 SQL 4트랙 내용 검토(파괴적 변경 없는지), refresh 원자화 시맨틱 검증. `bun run test`·`check`·`lint`·`build` 그린. + +--- + +## Phase 3 — 기능 추가 (이메일 인증 · 보안 알림 · 강제 로그아웃) + +**목적**: `email_verified` 클레임 정상화(현재 항상 false), 보안 이벤트 사용자 통지, admin 세션 강제 종료. +**파일 경계**: 3-1과 3-2가 `email.ts`를 공유 → 직렬(또는 같은 에이전트). 3-3은 독립. + +### [x] 3-1. F1 이메일 인증 플로우 — email_verification_tokens 3방언+마이그레이션 생성(0022/0005×3), verify-email 라우트(GET 비소진·POST 원자 소진), 재발송+미인증 배너, GC 연결 + +- 파일: `src/lib/server/email.ts`(템플릿 추가), `src/routes/(auth)/signup/+page.server.ts`, `src/routes/verify-email/+server.ts`(또는 `(auth)/verify-email/` — 기존 라우트 관례에 맞춤, 읽기 위임으로 확인), 스키마(인증 토큰 — `password_reset_tokens` 패턴 재사용해 신규 테이블 또는 범용화), `account/profile`(재발송 UI) +- 작업: (a) 가입 성공 시 인증 메일 발송(토큰 SHA-256 해시 저장, 24시간 TTL, rate-limit). (b) 검증 라우트: 토큰 검증 → `users.emailVerifiedAt` 세팅 → 완료 화면. (c) 재발송 액션(rate-limit, 이미 인증 시 no-op). (d) 미인증 사용자도 로그인은 허용(비파괴적) — account 화면에 미인증 배너+재발송 버튼. (e) i18n ko/en, 스키마 변경은 `db:generate:all`까지만. +- 수용 기준: 신규 가입→메일 링크→`email_verified=true` 클레임 확인 가능(토큰/userinfo). 토큰 1회용·만료 준수. 기존 사용자 로그인 무영향. 이메일 템플릿은 기존 escape/safe-URL 패턴 준수. 발송 실패가 가입 자체를 실패시키지 않음. + +### [x] 3-2. F2 보안 알림 메일 — 9종 이벤트(security-notify.ts, waitUntil 격리, users.locale 기반 ko/en), errors.rate_limit 잠복 키 부재 버그도 함께 해소 + +- 파일: `src/lib/server/email.ts`(알림 템플릿), 발송 지점: `(auth)/reset-password`(완료 시), `account/mfa/+page.server.ts`(TOTP 등록·삭제·백업코드 재생성), `account/passkeys/+page.server.ts`(등록·삭제), `admin/users/+page.server.ts`·`admin/users/[id]/+page.server.ts`(resetPassword·updateStatus 잠금 시) +- 작업: 공용 `sendSecurityNotification(...)` 헬퍼 + 이벤트별 문구(i18n은 수신자 locale 기반 — 사용자 profile locale 활용 가능 여부 읽기 위임으로 확인, 불가하면 ko 기본+en 병기). **발송은 best-effort**(실패해도 본 동작 성공, waitUntil 활용). +- 수용 기준: 나열된 각 이벤트에서 대상 사용자에게 메일 발송 경로 존재. 발송 실패가 본 액션을 실패시키지 않음. 이메일 주소 없는 계정은 조용히 스킵. 템플릿 escape 준수. + +### [x] 3-3. F3 admin 강제 로그아웃 액션 — forceLogout(세션+refresh 폐기, tenant 가드, audit, confirm UI) + +- 파일: `src/routes/admin/users/[id]/+page.server.ts`(액션 추가), `admin/users/[id]/+page.svelte`(버튼), `src/lib/server/auth/session.ts`(`revokeAllUserSessions` 재사용) +- 작업: 상태/비번 변경 없이 세션만 전체 폐기하는 전용 액션(확인 다이얼로그, audit `user_sessions_revoked` 기록, refresh token cascade 포함 — 기존 revoke 유틸 시맨틱 확인). i18n ko/en. +- 수용 기준: 대상 사용자의 모든 세션+refresh token 폐기. audit 기록. `assertUserInTenant` 가드 적용. UI 무결(기존 페이지 액션 패턴 준수). + +### [x] 3-검증 (독립 Opus 에이전트) — 통과(2026-07-06): V1~V6 전부 VERIFIED, 게이트 그린(prettier 2건 정리 완료). 참고: 재발송 시 기존 미사용 토큰 병존은 rate-limit+TTL로 한정(결함 아님) + +- 이메일 인증 토큰 보안(해시 저장·1회용·만료·rate-limit), 발송 best-effort 격리, 강제 로그아웃 tenant 가드 집중 리뷰. `bun run test`·`check`·`lint`·`build` 그린. 마이그레이션 생성물 확인(적용 안 함). + +--- + +## Phase 4 — 테스트 확충 (P0/P1 + 인프라) + +**목적**: 보안 핵심 로직 회귀 안전망 구축. Phase 1~3 신규 로직 포함. +**의존성**: Phase 1~3 완료 후 착수(신규 로직 테스트 포함 위해). 태스크 간 파일 독립 → 병렬 가능. + +### [x] 4-1. T1a 암호·토큰 코어 테스트 — crypto-keys(22)·password(19), WebCrypto 독립 교차검증, argon2 레거시 포함 + +- 파일: `test/unit/crypto-keys.test.ts`(신규), `test/unit/password.test.ts`(신규) +- 작업: `crypto/keys.ts` — JWT 서명/검증 라운드트립(RS256, typ/aud/exp), HMAC access token 발급/검증/변조 거부, private JWK 래핑/언래핑 라운드트립, HKDF 도메인 분리(용도별 키 상이). `auth/password.ts` — scrypt 해시/검증, 오답 거부, 레거시(argon2id/PBKDF2) 검증→scrypt 자동 업그레이드, PBKDF2 하한 거부, 상수시간 비교 경로. +- 수용 기준: 실제 WebCrypto/node:crypto로 동작(mock 최소화). 전체 스위트 그린. + +### [x] 4-2. T1b OIDC grant/refresh/세션/rate-limit 테스트 — grant(6)·refresh(8)·ratelimit(7)·email-verification(6) + session(9)·gc(7)는 4-3에서. mysql 분기는 d1 하네스 한계로 미커버(보고됨) + +- 파일: `test/unit/oidc-grant.test.ts`, `test/unit/oidc-refresh.test.ts`, `test/unit/session.test.ts`, `test/unit/ratelimit.test.ts` (모두 신규) +- 작업: 기존 crud-factory 테스트의 mock DB 패턴 재사용 — grant 1회 소진·만료 거부·codeHash 검증, refresh 회전·재사용 감지 family 폐기·원자화(2-3 결과) 경로, 세션 생성/검증/만료/revoke cascade, 슬라이딩 윈도우 경계(윈도우 전환·한도 초과·`login:user:` 키), Phase 1 신규 로직(더미 해시 경로·code 교환 세션 검사)과 Phase 3 이메일 인증 토큰 로직. +- 수용 기준: 각 모듈 핵심 분기 커버. mock DB 계약이 실제 drizzle 호출 shape와 일치(기존 패턴 준수). 전체 그린. + +### [x] 4-3. T2 schema-parity 강화 — nullable·타입 계열·인덱스/unique 비교, 예외 목록(signing_keys mysql 1건), drift 주입 자가검증 통과 + +- 파일: `test/unit/schema-parity.test.ts` +- 작업: 컬럼명 집합 비교에 더해 — 컬럼 타입 계열/nullable/default 유무 비교, 인덱스·unique 제약 이름/컬럼 비교. 방언별 정당한 차이(mysql partial-unique 불가 등)는 **명시적 예외 목록**으로 테스트 코드에 문서화(신규 drift만 실패). +- 수용 기준: 현 스키마로 그린(기존 known-gap은 예외 목록에). 가상의 drift(한 방언만 인덱스 누락) 주입 시 실패함을 확인. + +### [x] 4-4. T3 테스트 인프라·스크립트 — test:coverage(v8)·typecheck(tsc)·lint:fix·db:check(4방언), lefthook eslint --cache. 커버리지: 핵심 모듈 74~90%(keys 74.5/password 90.2/session 87/ratelimit 86.4), 전체 29%(미테스트 I/O 모듈 0% 영향) + +- 파일: `package.json`, `lefthook.yml`, `vitest.config.ts` +- 작업: `@vitest/coverage-v8` 추가 + `test:coverage` 스크립트, `lint:fix`, `db:check`(drizzle-kit check 4방언). lefthook pre-commit에 staged 파일 eslint 추가(느리면 --cache). CI에 coverage 아티팩트는 선택(과설계 금지 — 스크립트만 우선). +- 수용 기준: 각 스크립트 정상 동작. pre-commit이 정상 커밋 흐름을 과도하게 늦추지 않음(수 초 내). + +### [x] 4-검증 (독립 Opus 에이전트) — 통과(2026-07-06): 비-tautology 표본 검증, skip/only 0, 게이트 8종 그린 + +- 테스트가 실제 로직을 검증하는지(tautology/mock-echo 테스트 아닌지) 샘플 리뷰, `test.skip`/`.only` 0건, 전체 `bun run test` + `check`+`lint`+`build` 그린, coverage 리포트 생성 확인. + +--- + +## Phase 5 — 품질·문서 정리 (Phase 1~4와 독립, 병렬 가능) + +**목적**: 파일 비대 해소, admin 언어 혼재 해소, 보안 문서 격리, 문서·메모리 drift 정리. +**파일 경계**: 5-2와 5-3이 admin 라우트 겹침 가능 → 5-2 먼저(구조 확정) 후 5-3. + +### [x] 5-1. Q1 보안 감사 문서 삭제 (승인됨) — git rm 완료, 잔존 참조 0건 + +- 파일: `docs/security-audit-2026-05-12.md`, `docs/security-pentest-report.md` +- 작업: `git rm` 두 파일. README/다른 문서에서 이 파일들 참조가 있으면 제거. (히스토리 잔존 한계는 커밋 메시지에 명시하지 않음 — 문서 위치만 정리.) +- 수용 기준: 워킹트리에서 두 파일 제거, 깨진 참조 0건. + +### [x] 5-2. Q2 `users/[id]/+page.server.ts` 분리 — 677→181줄, user-actions/{profile,org,service,security}.ts 4모듈, 순수 이동 독립 검증 통과(11개 액션 계약 동일) + +- 파일: `src/routes/admin/users/[id]/+page.server.ts` (677줄) → `src/lib/server/admin/user-actions/{profile,org,service}.ts`(또는 라우트 인접 모듈 — 리포 관례에 맞춰 읽기 위임으로 결정) +- 작업: actions 10개를 도메인별 모듈로 분리, `+page.server.ts`는 조립만. **동작·에러 shape·audit kind 완전 보존**(순수 이동 리팩터). load 함수도 비대하면 함께 정리. +- 수용 기준: diff가 이동 중심(로직 변경 0). 모든 액션 이름·계약 무변경. `check`·`lint`·`build` 그린. + +### [x] 5-3. Q3 admin 에러 i18n + 공통 헬퍼 — adminError/requireFormId 헬퍼, 98곳 전환, ko/en 605키 대칭. 잔존(의도적 범위 외): crud-factory/schemas 한국어(테스트 계약과 충돌해 유지, 후속 시 테스트와 함께 전환), saml-sps/[id] 7건, validation.ts reason 문자열, 프로토콜 API 에러 + +- 파일: `src/routes/admin/{users,users/[id],oidc-clients,oidc-clients/[id],ldap-providers,skins,signing-keys,saml-sps,login}/+page.server.ts`, `src/lib/i18n/{ko,en}.json`, 공통 헬퍼(`src/lib/server/admin/form.ts` 신규 등) +- 작업: (a) `requireFormId` 류 공통 헬퍼 추출(중복 문구 12곳+). (b) admin 서버 액션의 하드코딩 한국어 에러를 auth 서버와 동일한 `translate(locals.locale, ...)` 패턴으로 전환, ko/en 키 대칭 추가. 프로토콜 레이어(oidc/saml/webauthn API 에러)는 이번 범위 제외(운영자·개발자 대상, 후속). +- 수용 기준: admin 라우트 서버 에러 하드코딩 한국어 0건(grep 근거). ko/en 키 대칭 유지(기존 대칭성 테스트 있으면 통과). 에러 shape(`{error}`/`{create:true,error}`) 무변경. + +### [x] 5-4. Q4 문서·메모리 정합 — improvement-report 정오표 추가, deferred-followups 상태 갱신, SECRET_ROTATION.md 신설(읽기 위임 사실관계 기반: 7용도·재암호화 3종·무중단 회전 불가 명시), 프로젝트 메모리 최신화 + +- 파일: `docs/improvement-report-2026-07-02.md`(stale 표기 갱신 — refresh token/OIDC scope/TOTP TOCTOU/E9 처리 현황), `docs/plans/deferred-followups/TODO.md`(1행 "커밋 승인 대기" → 병합 완료로), `docs/SECRET_ROTATION.md`(신규 — `IDP_SIGNING_KEY_SECRET` 회전 절차: 용도 4가지 나열, 회전 시 영향·순서·재암호화 필요 대상), 프로젝트 메모리(`oidc-scope-expansion.md` — groups/address/organization 구현 완료로 갱신) +- 작업: 문서 갱신은 Fable가 직접 가능(기획 산출물·문서). 단 SECRET_ROTATION.md의 기술 사실관계(어떤 데이터가 이 시크릿으로 암호화돼 있는지)는 읽기 위임으로 확인 후 작성. +- 수용 기준: 문서와 코드 현실 일치. 메모리 갱신 완료. + +### [x] 5-검증 (독립 Opus 에이전트) — 통과(2026-07-06): W1~W5 전부 VERIFIED, APPROVE. 스코프 오염 0(전 변경 파일 5페이즈 귀속 확인) + +- 5-2 diff의 순수-이동 여부(로직 변경 혼입 없는지) 집중 리뷰, 5-3 grep 검증(한국어 하드코딩 잔존 0), `bun run test`·`check`·`lint`·`build` 그린. + +--- + +## 최종 게이트 & 커밋 (STEP 7-8) + +- [x] 전체 스위트: `bun run test`(151) · `bun run check`(0 err) · `bun run lint` · `bun run build` · `typecheck` · `db:check`(4방언) · `test:coverage` · `db:generate:all` no-op — 전부 그린 (2026-07-06 최종 검증) +- [ ] 마이그레이션: **적용은 사용자 몫** — d1: `drizzle/0021`(legacy code drop+client_skins 보정)·`0022`(email_verification_tokens) / pg·mysql·sqlite: 각 `0004`·`0005` +- [x] 커밋 승인 게이트 → 승인(2026-07-06), 페이즈별 원자 커밋 7개 완료(fix(security)/chore(db)/refactor(db)/feat(gc)/feat(email)/test/refactor(admin)+docs). 푸시는 하지 않음. diff --git a/docs/security-audit-2026-05-12.md b/docs/security-audit-2026-05-12.md deleted file mode 100644 index 33ca4f9..0000000 --- a/docs/security-audit-2026-05-12.md +++ /dev/null @@ -1,617 +0,0 @@ -# IDP 보안 감사 리포트 (레드팀 전수감사) - -**대상 프로젝트:** `mack-erel/idp` — SvelteKit + Cloudflare Workers + D1 기반 Identity Provider (OIDC / SAML / WebAuthn / LDAP) -**감사 일시:** 2026-05-12 -**감사 브랜치:** `feat/root-account-hub` (HEAD: `281717c`) -**감사 방식:** 외부 침입(red-team) 시나리오 기반 — 실제 익스플로잇 가능성 중심, 8개 영역 병렬 전수감사 -**감사 범위:** 110개 소스 파일 + 설정/시크릿/CI/배포/의존성 전수 -**감사 영역:** - -1. OIDC provider (authorize/token/userinfo/jwks/end-session/discovery + admin) -2. SAML provider (sso/slo/metadata + admin) -3. 인증/세션/MFA/WebAuthn -4. 관리자 UI / 조직(부서·팀·파트·직급) / DB 스키마 / 감사 로그 / 스킨 / LDAP -5. 설정 / 시크릿 / CI/CD / 배포 -6. XSS / CSP / 보안 헤더 / 프론트엔드 -7. 공통 API / 메일 / SSRF -8. 의존성 CVE / 공급망 - ---- - -## 1. Executive Summary - -본 IDP 의 **코드 품질은 일반 OSS IDP 평균보다 명백히 높다.** PKCE S256 강제, authorization code 원자 소진, WebAuthn challenge 1회용 + dummy publicKey timing equalization, SAML AuthnRequest replay + DTD/ENTITY 차단, exc-c14n, session fixation 방지, TOTP secret AES-GCM + userId AAD, LDAP filter/DN 이스케이프, redirect double-decoding 등 IDP 가 실수하기 쉬운 지점 대부분이 정확히 막혀 있다. - -그러나 **(1) 운영 위생 — `.env` 평문 시크릿**, **(2) 공급망 — CI workflow 자동 commit + PAT, 비공식 의존성 fork**, **(3) 관리자 멀티테넌트 IDOR / last-admin 보호 race**, **(4) 외부 신뢰 모델 — skin HTML, DNS, LDAP fork** 4축에서 사고가 터지면 코드 품질이 무의미해진다. 발견된 Critical 14건 중 9건이 이 4축에 집중되어 있다. - -### Risk Level: **CRITICAL** - -### 누적 발견 통계 - -| Severity | 합계 | 대표 사례 | -| ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Critical** | **11건** | CI workflow PAT exfiltration, authorization code 평문 저장, SAML SLO 서명 우회, find-password Host header injection, skin SSRF DNS rebinding, cross-tenant IDOR, 부서 트리 순환 | -| **High** | **25건** | wildcard redirect_uri, JWT typ/crit 미검증, CSRF_SKIP 광범위, audit log 무결성 부재, signing-key rotate race, argon2 파라미터 OWASP 미달, `@hicaru/argon2-pure.js` 0.0.x 가 패스워드 해싱 경로에 실사용 | -| **Medium** | **30건** | CSP form-action 사실상 무제한, csrf.trustedOrigins=["*"], SAML LogoutResponse XML 파싱 0, signup vs login 정규화 비대칭(homoglyph), `@yrneh_jang/ldapjs` self-fork upstream patch lag | -| **Low** | **22건** | HSTS preload 미설정, COEP 미설정, tsconfig strict 옵션 부족 | - -### 정정 사항 (2026-05-12 본 리포트 작성 후 실측 결과 반영) - -| 원래 평가 | 실측 결과 | 정정 | -| ---------------------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------- | -| `nodemailer^8.0.5` 메이저 비실재 의심 | `nodemailer@8.0.7` 정상 존재 | 정상 | -| `typescript^6.0.2` 메이저 비실재 의심 | `6.0.3` 정상 | 정상 | -| `eslint^10`, `vite^8` 메이저 비실재 의심 | `10.3.0`, `8.0.12` 정상 | 정상 | -| `@yrneh_jang/ldapjs` 외부 maintainer fork (Critical) | `mack-erel` org 의 self-fork (`mack-erel/node-ldapjs`). 외부 공급망 아님 | **Medium 으로 강등** (upstream patch lag risk) | -| `@xmldom/xmldom 0.9.9` (4 high CVE) | `bun update` 로 `0.9.10` 적용 완료 (CVE 4건 해소) | 해결 완료 | - ---- - -## 1.1. 감사 중 즉시 적용된 조치 (Already Applied) - -- ✅ **`@xmldom/xmldom 0.9.9 → 0.9.10`** (`bun update`) — High 4건 CVE 해소 (GHSA-2v35-w6hq-6mfw / GHSA-f6ww-3ggp-fr8h / GHSA-x6wf-f3px-wcqx / GHSA-j759-j44w-7fr8) -- ✅ **git history 시크릿 검사 통과**: `gitleaks detect --log-opts="--all"` → `no leaks found` (false positive 1건 `.gitleaksignore` 처리) -- ✅ **CI 의존성 audit 게이트 추가**: `.github/workflows/ci.yml` 에 `bun audit --audit-level=high` 단계 추가 → High 이상 신규 발견 시 PR 머지 차단 -- ✅ **남은 의존성 vuln 3건** (postcss/esbuild/cookie): 모두 transitive dev-only + practical risk 낮음으로 무시 결정. Major upstream (vite/wrangler/kit) 다음 릴리스에서 자연 해결 대기 -- ✅ **C-1, C-2 (.env / wrangler.jsonc 평문 시크릿) 등급 재평가**: 사용자 확인 결과 `.env` / `wrangler.jsonc` `vars` 의 값은 **로컬 dev 전용**이며 production 시크릿은 `wrangler secret put` 으로 별도 관리됨이 확인됨. Git history 도 깨끗. → **Critical 등급 제거** (dev 머신 위생 항목으로 강등, Low 분류). 회전 작업 불요. - -## 2. 사전 안전 조치 (Bash 권한이 거부되어 사용자 직접 실행 필요) - -```bash -# 시크릿 history 검사 -git log --all -p -- .env wrangler.jsonc \ - | grep -E "IDP_SIGNING_KEY_SECRET|CLOUDFLARE_D1_TOKEN|github_pat_|SMTP_PASSWORD" -gitleaks detect --log-opts="--all" --no-git=false --redact - -# 의존성 CVE 실측 -bun audit -bunx osv-scanner --lockfile=bun.lock -bun pm ls --all | grep -E "nodemailer|argon2|ldapjs|typescript|eslint|vite" - -# 비실재/비정상 메이저 확인 -npm view eslint versions --json | tail -20 -npm view typescript versions --json | tail -10 -npm view vite versions --json | tail -10 -npm view nodemailer versions --json | tail -10 -npm view @yrneh_jang/ldapjs maintainers time - -# 운영 secret 위치 확인 -wrangler secret list -``` - ---- - -## 3. Critical Findings (11건) — 즉시 조치 - -### ~~C-1. `.env` 평문 / C-2. `wrangler.jsonc` `vars`~~ → **등급 제거 (Low / 운영 위생)** - -**원래 Severity:** Critical → **재평가 후 Low** - -**재평가 사유:** 사용자 확인 결과 `.env` 와 `wrangler.jsonc` `vars` 의 값은 **로컬 dev 전용**이며 production 시크릿은 `wrangler secret put` 으로 별도 관리됨이 확인됨. `.gitignore` 처리 정상, git history `gitleaks no leaks found`, 외부 노출 경로 없음. → 시크릿 회전 불요. - -**남는 권장 (Low):** - -- FileVault 디스크 암호화 ON -- iCloud/Dropbox/Time Machine 가 `.env` 가 들어있는 디렉토리 동기화 안 하는지 확인 -- IDE AI/telemetry 가 `.env` 안 읽도록 `.cursorignore` / IDE 설정 -- 향후 production 시크릿을 실수로 `.env` 에 넣지 않도록 `.env.example` 만 dev placeholder 유지 - ---- - -### C-3. CI `format` job 의 PAT 자동 commit 패턴 — 공급망 RCE - -**파일:** `/Users/henry/github/mack-erel/idp/.github/workflows/ci.yml:42-72` -**Severity:** Critical - -```yaml -- uses: actions/checkout@v6 - with: - token: ${{ secrets.WORKFLOW_PAT || secrets.GITHUB_TOKEN }} -- run: bun install -- run: bun run format -- uses: stefanzweifel/git-auto-commit-action@v7 -``` - -**공격 시나리오:** - -1. PR 작성자가 `prettier.config.js`, `prettier-plugin-tailwindcss`, 또는 `eslint.config.js` 를 악의적으로 수정 (prettier plugin 은 임의 JS). -2. `bun install` 또는 `bun run format` 실행 단계에서 임의 코드 실행. -3. `secrets.WORKFLOW_PAT` 가 환경에 주입되어 있어 탈취. -4. PAT 권한으로 main 직커밋 + branch protection 우회 가능 → 다음 deploy.yml 이 main push 트리거로 Cloudflare 자동 배포 → **IDP 자체 백도어 코드**. - -**Remediation:** - -- format 단계를 `workflow_run` 으로 분리해 격리, 또는 자동 commit 제거(작성자가 로컬에서 lint:fix). -- WORKFLOW_PAT 를 GitHub App token 으로 대체. -- `bun install --frozen-lockfile --ignore-scripts` 강제. - ---- - -### C-4. `deploy.yml` 의 shell injection 패턴 - -**파일:** `/Users/henry/github/mack-erel/idp/.github/workflows/deploy.yml:63-64` -**Severity:** Critical - -```yaml -- name: Setup Wrangler config - run: echo '${{ secrets.WRANGLER_JSONC }}' > wrangler.jsonc -``` - -시크릿이 단일 따옴표 안에 직접 보간 → 시크릿에 `'` 가 포함되면 shell quote 깨져 임의 명령 실행. GitHub Actions 마스킹은 출력에만 적용되며 syntax injection 은 막지 못한다. - -**Remediation:** - -```yaml -- name: Setup Wrangler config - env: - WRANGLER_JSONC: ${{ secrets.WRANGLER_JSONC }} - run: printf '%s' "$WRANGLER_JSONC" > wrangler.jsonc -``` - ---- - -### ~~C-5. `@yrneh_jang/ldapjs` 비공식 fork~~ → **Medium 으로 강등** (M-DEP-1 참조) - -**정정**: 실측 결과 `@yrneh_jang/ldapjs` 는 외부 maintainer 패키지가 아니라 **`mack-erel` org (이 IDP 와 동일 owner)** 의 self-fork (`mack-erel/node-ldapjs`) 임이 확인됨. Cloudflare Workers 호환을 위한 fork. - -→ "외부 npm maintainer 손상" 시나리오 소멸. Critical 등급 부적절. **Medium (upstream ldapjs 보안 패치 동기화 책임이 본인에게 있음)** 으로 강등하여 본 문서 후반의 Medium 섹션 참조. - ---- - -### C-6. Authorization code 가 D1 에 평문 저장 - -**파일:** `src/routes/oidc/authorize/+server.ts:150-165`, `src/lib/server/oidc/grant.ts:24-44` -**Category:** A02 / A04 -**Severity:** Critical - -```ts -const code = crypto.randomUUID().replace(/-/g, "") + crypto.randomUUID().replace(/-/g, ""); -await createGrant(db, { code, ... }); // ← 평문 저장 -``` - -**공격 경로:** D1 read-replica · 로그 dump · 백업 유출 · readonly admin. authorize 직후 5분 내 평문 code 읽히면 token 교환 가능. - -**PoC:** `SELECT code, client_id, redirect_uri FROM oidc_grants WHERE used_at IS NULL;` → `POST /oidc/token`. - -**Remediation:** code 는 client 에만 노출, DB 에는 SHA-256 해시 저장. - -```ts -const code = b64uEncode(crypto.getRandomValues(new Uint8Array(32))); -const codeHash = b64uEncode(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(code))); -await createGrant(db, { codeHash, ... }); -``` - ---- - -### C-7. `end-session` GET 의 인증 무관 confirm 페이지 + clickjacking - -**파일:** `src/routes/oidc/end-session/+server.ts:70-128` -**Severity:** Critical - -GET 핸들러가 `locals.user` 가 null 이어도 `id_token_hint` 만 valid 면 confirm 페이지를 렌더. `frame-ancestors` / `X-Frame-Options` 응답 헤더 부재. - -**공격:** - -```html - - -``` - -또는 phishing 메일로 IDP 공식 도메인의 confirm 페이지를 표시 → 사용자가 의심 없이 클릭 → 강제 로그아웃. - -**Remediation:** - -- 응답에 `X-Frame-Options: DENY`, `Content-Security-Policy: frame-ancestors 'none'; default-src 'none'; form-action 'self'`, `Cache-Control: no-store`. -- `locals.user` 미존재 시 GET 단계에서 204 반환. -- confirm form 에 server-signed CSRF token. - ---- - -### C-8. SAML SP cert 미등록 시 LogoutRequest 서명 검증 우회 - -**파일:** `src/routes/saml/slo/+server.ts:271-277` -**Severity:** Critical - -```ts -if (sp.cert) { - const valid = await verifySamlRedirectSignature(rawQuery, sp.cert); - if (!valid) throw error(400, "Invalid SAMLRequest signature"); -} -``` - -`sp.cert IS NULL` 이면 모든 SP-initiated `LogoutRequest` 가 **서명 검증 없이 신뢰**됨. 공격자가 임의 entityId 로 LogoutRequest 위조 → 피해자 IdP 세션 revoke + SLO 체인 트리거 → 모든 연결 SP 로 LogoutRequest 발사. - -**Remediation:** - -```ts -if (!sp.cert) throw error(400, "SP 인증서가 등록되지 않아 SLO 요청을 검증할 수 없습니다."); -const valid = await verifySamlRedirectSignature(rawQuery, sp.cert); -if (!valid) throw error(400, "Invalid SAMLRequest signature"); -if (!url.searchParams.has("Signature")) throw error(400, "LogoutRequest 는 반드시 서명되어야 합니다."); -``` - -또한 admin SP 등록 액션에서 SLO 활성 SP 는 cert 필수로 강제. - ---- - -### C-9. SAML SLO 체인 응답에 세션-state binding 없음 - -**파일:** `src/routes/saml/slo/+server.ts:160-225` -**Severity:** Critical - -`initiatingSpEntityId` 가 NULL 또는 SP cert NULL 일 때 LogoutResponse 서명 검증 완전 스킵. stateId 만 알면 누구나 SLO 체인 진전 가능 → IdP 가 서명한 LogoutRequest 임의 발사. - -**Remediation:** - -- LogoutResponse 도 SP cert 필수. -- IdP-initiated 체인의 경우 `locals.session?.id === state.idpSessionRecordId` 일관성 검사 추가. - ---- - -### C-10. `find-password` Host header injection → 계정 takeover - -**파일:** `src/routes/(auth)/find-password/+page.server.ts:105-112` -**Category:** A07 / Identity Recovery -**Severity:** Critical - -```ts -const issuer = (env.IDP_ISSUER_URL ?? event.url.origin).replace(/\.+$/, "").replace(/\/+$/, ""); -const resetUrl = `${issuer}/reset-password?${resetParams.toString()}`; -``` - -`IDP_ISSUER_URL` 누락 시 `event.url.origin` fallback 은 Host 헤더에 의존. 공격자가 `Host: evil.com` 으로 폼 제출 → victim 메일 본문에 `https://evil.com/reset-password?token=<유효토큰>` 박혀서 발송 → victim 이 클릭하면 토큰 유출 → **1시간 내 계정 takeover**. - -**Remediation:** - -```ts -const issuer = env.IDP_ISSUER_URL?.replace(/\/+$/, ""); -if (!issuer) return fail(503, { error: "메일 발송 설정 오류가 있습니다." }); -``` - ---- - -### C-11. 부서 트리 간접 순환 참조 무방어 - -**파일:** `src/routes/admin/departments/+page.server.ts:60-73` -**Severity:** Critical - -`parentId === id` 직접 순환만 차단. 간접 순환(A→B→A, A→B→C→A) 미검증 → 트리 traversal 무한루프 → Workers CPU 타임아웃, 권한 상속 계산 붕괴. - -**PoC:** - -``` -POST /admin/departments?/update { id=A, parentId=B } -POST /admin/departments?/update { id=B, parentId=A } ← 통과 -``` - -**Remediation:** update 전 `parentId` 부터 root 까지 traversal 하여 `id` 가 조상에 있는지 검사 + depth ≤ 8 제한. - ---- - -### C-12. Last-admin 보호 self/race 우회 - -**파일:** `src/routes/admin/users/[id]/+page.server.ts:160-251` -**Severity:** Critical - -`updateProfile` 의 self-prevention 이 "값 다를 때만" 차단. admin A, B 두 명 상황에서 A 가 B 의 role 변경 후 본인 갱신 race 시 last-admin 보호 우회 가능. - -**Remediation:** `assertNotLastAdmin` 을 self 케이스에도 호출. 자기 자신 role/status 변경 자체를 항상 차단 (값 일치 여부 무관). - ---- - -### C-13. `addAssignment` cross-tenant IDOR - -**파일:** `src/routes/admin/users/[id]/+page.server.ts:482-580` -**Severity:** Critical - -`addAssignment/addDept/addTeam/addPart` action 들이 `params.id` 의 tenant 검증 없이 `userServiceAssignments` INSERT. 테넌트 X admin 이 `/admin/users/` POST 하면 cross-tenant 권한 row 생성. 현재 single-tenant 라 임팩트 제한, **멀티테넌트 활성화 즉시 폭발**. - -**Remediation:** 모든 admin `[id]` action 진입부에서 `select users where id=params.id AND tenantId=tenant.id`. - ---- - -### C-14. Skin SSRF DNS rebinding + 외부 HTML `{@html}` 무제한 주입 - -**파일:** - -- `src/lib/server/skin/resolver.ts:26-44` (SSRF) -- `src/routes/(auth)/{login,signup,mfa,reset-password,find-id,find-password}/+page.svelte` (6개소 `{@html}`) - -**Severity:** Critical - -**SSRF DNS rebinding:** `isFetchUrlAllowed` 가 hostname 문자열만 검증. fetch 시점에 DNS 재해석되므로 `evil.com` 의 A 레코드를 일시적으로 공인 IP 로 응답해 통과 → 실제 요청 시 `10.0.0.1`. Workers `fetch()` 는 IP pinning 보장 안 함. - -**`{@html}` 주입:** admin 이 등록한 외부 https 호스트가 한 번이라도 침해되면 모든 로그인/MFA/회원가입 페이지에 임의 HTML 주입. CSP `script-src 'self'` 는 인라인 차단하지만 `form-action 'self' https:` 가 풀려있어 `` 그대로 통과 → credential 평문 전송. - -**Remediation:** - -- 단기: - - DOMPurify 로 sanitize: ` + +
+
+ + +
+

{t("account.sessions.title")}

+

{t("account.sessions.subtitle")}

+ + {#if formError} +
+ {formError} +
+ {/if} + + {#if revoked} +
+ {t("account.sessions.revoked_notice")} +
+ {/if} + + {#if revokedOthers} +
+ {t("account.sessions.revoked_others_notice")} +
+ {/if} + + {#if data.sessions.length > 0} +
+ {#each data.sessions as s (s.id)} + {@const isCurrent = s.id === data.currentSessionId} +
+
+
+ {#if isCurrent} + + {t("account.sessions.current_badge")} + + {/if} +

+ {t("account.sessions.device_label")}: {s.userAgent ?? t("account.sessions.unknown")} +

+

+ {t("account.sessions.ip_label")}: {s.ip ?? t("account.sessions.unknown")} +

+

+ {t("account.sessions.last_seen_label")}: {formatWhen(s.lastSeenAt)} +

+

+ {t("account.sessions.created_label")}: {formatWhen(s.createdAt)} +

+
+ + + + + +
+
+ {/each} +
+ {:else} +
+

{t("account.sessions.empty")}

+
+ {/if} + + {#if otherCount > 0} +
+ +
+ {/if} +
+
+
From 273119868a2287f7cb208e5f4d02bcd0df1f8b64 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 13:43:16 +0900 Subject: [PATCH 10/26] =?UTF-8?q?feat(admin):=20P7=20=EA=B3=84=EC=A0=95=20?= =?UTF-8?q?=EC=B4=88=EB=8C=80=20=ED=94=8C=EB=A1=9C=EC=9A=B0=20=E2=80=94=20?= =?UTF-8?q?=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=97=86=EC=9D=B4=20?= =?UTF-8?q?=EC=84=A0=EC=83=9D=EC=84=B1=20=ED=9B=84=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - issueInvite: 관리자가 비밀번호 없이 계정 선생성 → invite_token(72h·1회용) 발급 → sendInviteEmail - /accept-invite 라우트: 토큰 검증(isNull(usedAt)+미만료) 후 최초 비밀번호 설정, 원자 소비(usedAt 세팅) - admin/users: 초대 액션 + "초대중" 배지(유효 invite_token 보유자만 판정 — 연합 사용자 오표시 해소) - email.ts: sendInviteEmail(안전 URL 가드·72h 안내 문구) Co-Authored-By: Claude Fable 5 --- src/lib/server/auth/invite.ts | 35 ++++++ src/lib/server/email.ts | 18 +++ .../(auth)/accept-invite/+page.server.ts | 99 +++++++++++++++ src/routes/(auth)/accept-invite/+page.svelte | 71 +++++++++++ src/routes/admin/users/+page.server.ts | 113 +++++++++++++++++- src/routes/admin/users/+page.svelte | 88 +++++++++++++- 6 files changed, 417 insertions(+), 7 deletions(-) create mode 100644 src/lib/server/auth/invite.ts create mode 100644 src/routes/(auth)/accept-invite/+page.server.ts create mode 100644 src/routes/(auth)/accept-invite/+page.svelte diff --git a/src/lib/server/auth/invite.ts b/src/lib/server/auth/invite.ts new file mode 100644 index 0000000..4539cd4 --- /dev/null +++ b/src/lib/server/auth/invite.ts @@ -0,0 +1,35 @@ +import { env } from "$env/dynamic/private"; +import type { DB } from "$lib/server/db"; +import { inviteTokens } from "$lib/server/db/schema"; +import { generateToken, sendInviteEmail } from "$lib/server/email"; + +export const INVITE_EXPIRY_MS = 72 * 60 * 60 * 1000; // 72시간 + +/** + * 초대 토큰 발급 + 초대 메일 발송(admin invite 액션에서 사용). + * email-verification.ts 의 issueEmailVerification 과 동일한 격리 규약을 따른다: + * - issuer(IDP_ISSUER_URL) 미설정 시 발송 스킵(host header injection 방지 목적). + * - Workers 는 waitUntil 로 응답 경로에서 분리, Node 는 await. + * - 토큰 발급/발송 예외는 삼켜서 호출부(계정 생성 응답)에 전파하지 않는다(best-effort). + */ +export async function issueInvite(db: DB, userId: string, email: string, platform: App.Platform | undefined): Promise { + const issuer = env.IDP_ISSUER_URL?.replace(/\.+$/, "").replace(/\/+$/, ""); + if (!issuer) { + console.error("[invite] IDP_ISSUER_URL 미설정 — 초대 메일 발송 불가"); + return; + } + try { + const { token, tokenHash } = await generateToken(); + const expiresAt = new Date(Date.now() + INVITE_EXPIRY_MS); + await db.insert(inviteTokens).values({ userId, tokenHash, expiresAt }); + const inviteUrl = `${issuer}/accept-invite?token=${encodeURIComponent(token)}`; + const sendPromise = sendInviteEmail(email, inviteUrl, platform).catch(() => { + // 발송 실패는 조용히 무시 + }); + const wait = platform?.ctx?.waitUntil?.bind(platform.ctx); + if (wait) wait(sendPromise); + else await sendPromise; + } catch { + // 토큰 발급/발송 실패가 상위 흐름을 실패시키지 않도록 격리 + } +} diff --git a/src/lib/server/email.ts b/src/lib/server/email.ts index e9a66ac..6b7b245 100644 --- a/src/lib/server/email.ts +++ b/src/lib/server/email.ts @@ -178,6 +178,24 @@ export async function sendEmailVerificationEmail(to: string, verifyUrl: string, await send(to, "이메일 인증 안내", html, text, platform); } +export async function sendInviteEmail(to: string, inviteUrl: string, platform?: App.Platform): Promise { + const safeUrl = safeAbsoluteUrl(inviteUrl); + if (!safeUrl) { + // 잘못된 URL 형식이면 메일 발송 자체 거부. + console.error("[email] sendInviteEmail: 잘못된 inviteUrl scheme — 발송 취소"); + return; + } + const html = baseHtml( + "계정 초대", + `

계정에 초대되었습니다. 아래 버튼을 클릭하여 비밀번호를 설정하고 가입을 완료하세요. 링크는 72시간 동안 유효합니다.

+

+ 초대 수락하기 +

`, + ); + const text = `계정에 초대되었습니다. 아래 링크에서 비밀번호를 설정하고 가입을 완료하세요. 링크는 72시간 동안 유효합니다.\n\n${safeUrl}\n\n본인이 요청하지 않았다면 이 이메일을 무시해 주세요.`; + await send(to, "계정 초대 안내", html, text, platform); +} + // ── 보안 알림 메일 (best-effort) ───────────────────────────────────────────── // 문구는 호출부에서 수신자 locale 로 번역해 넘긴다(email.ts 는 i18n 에 결합하지 않는다). // baseHtml 의 고정 한국어 푸터 대신 전달받은 localized 문구로 본문을 구성한다. diff --git a/src/routes/(auth)/accept-invite/+page.server.ts b/src/routes/(auth)/accept-invite/+page.server.ts new file mode 100644 index 0000000..4c3fa4e --- /dev/null +++ b/src/routes/(auth)/accept-invite/+page.server.ts @@ -0,0 +1,99 @@ +import { fail } from "@sveltejs/kit"; +import { and, eq, isNull } from "drizzle-orm"; +import type { Actions, PageServerLoad } from "./$types"; +import { requireDbContext } from "$lib/server/auth/guards"; +import { users, inviteTokens, credentials } from "$lib/server/db/schema"; +import { hashToken } from "$lib/server/email"; +import { hashPassword } from "$lib/server/auth/password"; +import { PASSWORD_CREDENTIAL_TYPE } from "$lib/server/auth/constants"; +import { runAtomic } from "$lib/server/db/atomic"; +import { checkRateLimit } from "$lib/server/ratelimit"; +import { recordAuditEvent, getRequestMetadata } from "$lib/server/audit"; +import { translate } from "$lib/i18n/server"; + +// 초대 토큰 상태를 tenant 범위에서 조회한다(read-only, 비소진). 유효하면 record, 아니면 null. +async function lookupToken(db: App.Locals["db"], tenantId: string, token: string) { + if (!db) return null; + const tokenHash = await hashToken(token); + const now = new Date(); + const [record] = await db + .select({ tokenId: inviteTokens.id, userId: users.id, expiresAt: inviteTokens.expiresAt }) + .from(inviteTokens) + .innerJoin(users, eq(inviteTokens.userId, users.id)) + .where(and(eq(inviteTokens.tokenHash, tokenHash), isNull(inviteTokens.usedAt), eq(users.tenantId, tenantId))) + .limit(1); + if (!record || record.expiresAt < now) return null; + return record; +} + +export const load: PageServerLoad = async ({ locals, url }) => { + const token = url.searchParams.get("token"); + if (!token || !locals.db || !locals.tenant) { + return { valid: false, token: null as string | null }; + } + const record = await lookupToken(locals.db, locals.tenant.id, token); + if (!record) return { valid: false, token: null as string | null }; + return { valid: true, token: token as string | null }; +}; + +export const actions: Actions = { + default: async (event) => { + const { db, tenant } = requireDbContext(event.locals); + const locale = event.locals.locale; + + const formData = await event.request.formData(); + const token = String(formData.get("token") ?? ""); + const password = String(formData.get("password") ?? ""); + const confirmPassword = String(formData.get("confirmPassword") ?? ""); + + // 토큰 제출 브루트포스 방어 — 형제 인증 라우트와 동일하게 IP 당 제한. + const meta = getRequestMetadata(event); + const rl = await checkRateLimit(db, `accept-invite:${meta.ipKey}`, { windowMs: 15 * 60 * 1000, limit: 10 }); + if (!rl.allowed) { + return fail(429, { error: translate(locale, "errors.rate_limit", { minutes: Math.ceil(rl.retryAfterMs / 60000) }) }); + } + + if (!token) return fail(400, { error: translate(locale, "accept_invite.invalid_link") }); + // 비밀번호 정책 재사용 — reset_password 와 동일(8자 이상, 확인 일치). + if (password.length < 8) return fail(400, { error: translate(locale, "accept_invite.err_password_short") }); + if (password !== confirmPassword) return fail(400, { error: translate(locale, "accept_invite.err_password_mismatch") }); + + const record = await lookupToken(db, tenant.id, token); + if (!record) return fail(400, { error: translate(locale, "accept_invite.invalid_link") }); + + const now = new Date(); + const hashedPw = await hashPassword(password); + + // 초대 클릭 = 이메일 소유 증명이므로 emailVerifiedAt 을 함께 세팅(별도 이메일 인증 스킵). + // 최초 비밀번호 설정(credentials insert) + emailVerifiedAt 세팅 + 토큰 소진을 원자적으로. + // 같은 user 의 미사용 초대 토큰을 모두 소진해 재사용을 차단한다. + await runAtomic(db, [ + (h) => + h.insert(credentials).values({ + id: crypto.randomUUID(), + userId: record.userId, + type: PASSWORD_CREDENTIAL_TYPE, + secret: hashedPw, + label: "비밀번호", + }), + (h) => h.update(users).set({ emailVerifiedAt: now, updatedAt: now }).where(eq(users.id, record.userId)), + (h) => + h + .update(inviteTokens) + .set({ usedAt: now }) + .where(and(eq(inviteTokens.userId, record.userId), isNull(inviteTokens.usedAt))), + ]); + + await recordAuditEvent(db, { + tenantId: tenant.id, + userId: record.userId, + actorId: record.userId, + kind: "invite_accepted", + outcome: "success", + ip: meta.ip, + userAgent: meta.userAgent, + }); + + return { accepted: true }; + }, +}; diff --git a/src/routes/(auth)/accept-invite/+page.svelte b/src/routes/(auth)/accept-invite/+page.svelte new file mode 100644 index 0000000..f906d28 --- /dev/null +++ b/src/routes/(auth)/accept-invite/+page.svelte @@ -0,0 +1,71 @@ + + +
+
+
+
+

{t("accept_invite.title")}

+

{t("accept_invite.subtitle")}

+
+ + {#if accepted} +
+ {t("accept_invite.success")} +
+ + + {t("accept_invite.go_login")} → + + {:else if data.valid} + {#if err} +
{err}
+ {/if} +
+ +
+ + +
+
+ + +
+ +
+ {:else} +
+ {t("accept_invite.invalid_link")} +
+ + + {t("accept_invite.go_login")} → + + {/if} +
+
diff --git a/src/routes/admin/users/+page.server.ts b/src/routes/admin/users/+page.server.ts index 514acf8..f9fe9b0 100644 --- a/src/routes/admin/users/+page.server.ts +++ b/src/routes/admin/users/+page.server.ts @@ -1,15 +1,17 @@ import { fail } from "@sveltejs/kit"; -import { desc, eq, and, lt, or, sql } from "drizzle-orm"; +import { desc, eq, and, lt, gt, or, sql, inArray, isNull } from "drizzle-orm"; import type { Actions, PageServerLoad } from "./$types"; import { requireAdminContext, assertNotLastAdmin } from "$lib/server/auth/guards"; import { recordAuditEvent, getRequestMetadata } from "$lib/server/audit/index"; import { dispatchSecurityAlert } from "$lib/server/security-notify"; -import { users, credentials } from "$lib/server/db/schema"; +import { users, credentials, inviteTokens } from "$lib/server/db/schema"; import { hashPassword } from "$lib/server/auth/password"; import { normalizeEmail, normalizeUsername } from "$lib/server/auth/users"; import { PASSWORD_CREDENTIAL_TYPE } from "$lib/server/auth/constants"; import { revokeAllUserSessions } from "$lib/server/auth/session"; import { adminError, requireFormId } from "$lib/server/admin/errors"; +import { runAtomic } from "$lib/server/db/atomic"; +import { issueInvite } from "$lib/server/auth/invite"; const PAGE_SIZE = 50; @@ -59,7 +61,37 @@ export const load: PageServerLoad = async ({ locals, url }) => { const rows = hasMore ? rowsPlusOne.slice(0, PAGE_SIZE) : rowsPlusOne; const nextCursor = hasMore && rows.length > 0 ? rows[rows.length - 1].createdAt.getTime() : null; - return { users: rows, search, pageSize: PAGE_SIZE, nextCursor }; + // "초대중" 배지 판정: 실제 미사용·미만료 invite_token 을 보유한 사용자만. + // + // 과거 판정(emailVerifiedAt NULL AND password credential 부재)은 LDAP/OIDC/SAML/패스키 전용 + // 계정도 만족한다(연합 프로비저닝은 emailVerifiedAt NULL + password credential 없이 계정 생성). + // 그래서 연합 사용자 전원이 "초대중"으로 오표시됐다. 진짜 초대 대기 계정은 accept-invite 로 + // 소비될(usedAt 세팅) 유효 토큰을 갖는다 — accept-invite 서버의 유효성 판정(isNull(usedAt) + + // 미만료)과 동일 기준으로 이번 페이지 사용자 id 범위만 배치 조회(N+1 없음)한다. + const pageUserIds = rows.map((r) => r.id); + const pendingInviteUserIds = new Set(); + if (pageUserIds.length > 0) { + const now = new Date(); + const inviteRows = await db + .select({ userId: inviteTokens.userId }) + .from(inviteTokens) + .where(and(inArray(inviteTokens.userId, pageUserIds), isNull(inviteTokens.usedAt), gt(inviteTokens.expiresAt, now))); + for (const t of inviteRows) pendingInviteUserIds.add(t.userId); + } + + const usersOut = rows.map((r) => ({ + id: r.id, + username: r.username, + email: r.email, + displayName: r.displayName, + role: r.role, + status: r.status, + createdAt: r.createdAt, + // 초대 대기 상태(유효한 미사용 초대 토큰 보유) 여부. + pendingInvite: pendingInviteUserIds.has(r.id), + })); + + return { users: usersOut, search, pageSize: PAGE_SIZE, nextCursor }; }; export const actions: Actions = { @@ -140,6 +172,81 @@ export const actions: Actions = { return { create: true }; }, + // ── 사용자 초대 ──────────────────────────────────────────────────────────── + // 비밀번호 없이 계정을 선생성(credentials row 생략, emailVerifiedAt NULL, status active)한 뒤 + // 초대 토큰+메일을 발급한다. 초대 링크 수락 시 최초 비밀번호가 설정된다(accept-invite). + invite: async (event) => { + const { locals } = event; + const { db, tenant } = requireAdminContext(locals); + const locale = locals.locale; + + const fd = await event.request.formData(); + const email = normalizeEmail(String(fd.get("email") ?? "")); + const username = normalizeUsername(String(fd.get("username") ?? "")) || email.split("@")[0]; + const displayName = String(fd.get("displayName") ?? "").trim(); + const role = String(fd.get("role") ?? "user") as "admin" | "user"; + + if (!email) { + return fail(400, { invite: true, error: adminError(locale, "email_required") }); + } + if (!["admin", "user"].includes(role)) { + return fail(400, { invite: true, error: adminError(locale, "invalid_role") }); + } + + // 중복 확인 — create 액션과 동일 규약(이메일/아이디 중복 차단). + const [existing] = await db + .select({ id: users.id }) + .from(users) + .where(and(eq(users.tenantId, tenant.id), eq(users.email, email))) + .limit(1); + if (existing) { + return fail(409, { invite: true, error: adminError(locale, "email_taken") }); + } + + const [existingUsername] = await db + .select({ id: users.id }) + .from(users) + .where(and(eq(users.tenantId, tenant.id), eq(users.username, username))) + .limit(1); + if (existingUsername) { + return fail(409, { invite: true, error: adminError(locale, "username_taken") }); + } + + const userId = crypto.randomUUID(); + // users insert 를 runAtomic 로 감싼다 — 지금은 단일 write 지만, 초대 계정에 후속 row(예: 기본 + // 프로필/그룹)를 원자적으로 함께 넣도록 확장할 때 create 액션의 비원자 패턴을 답습하지 않기 위함. + await runAtomic(db, [ + (h) => + h.insert(users).values({ + id: userId, + tenantId: tenant.id, + email, + username, + displayName: displayName || null, + role, + status: "active", + // emailVerifiedAt 미설정(NULL) — 초대 링크 수락 시 세팅된다. + }), + ]); + + // 초대 토큰 발급 + 메일(best-effort, 완전 격리). 발송 실패가 초대 응답을 실패시키지 않는다. + await issueInvite(db, userId, email, event.platform); + + const requestMetadata = getRequestMetadata(event); + await recordAuditEvent(db, { + tenantId: tenant.id, + userId, + actorId: locals.user!.id, + kind: "user_invited", + outcome: "success", + ip: requestMetadata.ip, + userAgent: requestMetadata.userAgent, + detail: { email, role }, + }); + + return { invite: true }; + }, + // ── 상태 변경 ───────────────────────────────────────────────────────────── updateStatus: async (event) => { const { locals } = event; diff --git a/src/routes/admin/users/+page.svelte b/src/routes/admin/users/+page.svelte index c7730cf..bb19b3a 100644 --- a/src/routes/admin/users/+page.svelte +++ b/src/routes/admin/users/+page.svelte @@ -12,10 +12,12 @@ const dateFormatter = new Intl.DateTimeFormat("ko-KR", { }); let showCreate = $state(false); +let showInvite = $state(false); let resetPasswordUserId = $state(null); const formErr = $derived((form as { error?: string } | null)?.error ?? null); const createErr = $derived((form as { create?: boolean; error?: string } | null)?.create ? formErr : null); +const inviteErr = $derived((form as { invite?: boolean; error?: string } | null)?.invite ? formErr : null); const resetErr = $derived((form as { resetPassword?: boolean; error?: string } | null)?.resetPassword ? formErr : null); const STATUS_COLOR: Record = { @@ -35,11 +37,84 @@ const STATUS_NEXT: Record = {

{t("users.title")}

- +
+ + +
+ {#if showInvite} +
+

{t("users.invite_title")}

+

{t("users.invite_hint")}

+ + {#if inviteErr} +
+ {inviteErr} +
+ {/if} + +
{ + return ({ result, update }) => { + update(); + if (result.type === "success") showInvite = false; + }; + }} + class="grid grid-cols-1 gap-3 sm:grid-cols-2"> +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ {/if} + {#if showCreate}

{t("users.create_title")}

@@ -100,7 +175,7 @@ const STATUS_NEXT: Record = {
{/if} - {#if formErr && !createErr && !resetErr} + {#if formErr && !createErr && !inviteErr && !resetErr}
{formErr}
@@ -164,6 +239,11 @@ const STATUS_NEXT: Record = { {t(`users.status_${user.status}`)} + {#if user.pendingInvite} + + {t("users.badge_pending_invite")} + + {/if} {#if STATUS_NEXT[user.status]}
From 5799c81561c043ab16c03ae2820d709e2d92a746 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 13:45:33 +0900 Subject: [PATCH 11/26] =?UTF-8?q?feat(account):=20P8=20=EA=B3=84=EC=A0=95?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C(=EC=86=8C=ED=94=84=ED=8A=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20+=20=EC=9C=A0=EC=98=88=20=EB=B3=B5=EA=B5=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /account/danger-zone: 셀프서비스 탈퇴 요청 → status=deletion_pending + deletionScheduledAt(유예 30일), 세션·refresh 폐기 - authenticatePendingDeletionUser + 로그인 복구 흐름: 유예 내 재로그인 시 2단계 확인 후 active 환원(deletionScheduledAt=null), 유예 경과 시 거부 - gc: deletion_pending & deletionScheduledAt --- src/lib/server/admin/user-actions/profile.ts | 5 +- src/lib/server/auth/guards.ts | 4 +- src/lib/server/auth/users.ts | 38 ++++ src/lib/server/db/gc.ts | 12 +- src/lib/server/security-notify.ts | 3 +- src/routes/(auth)/login/+page.server.ts | 55 +++++- src/routes/(auth)/login/+page.svelte | 181 +++++++++++------- .../account/danger-zone/+page.server.ts | 152 +++++++++++++++ src/routes/account/danger-zone/+page.svelte | 85 ++++++++ test/unit/gc.test.ts | 23 +++ 10 files changed, 481 insertions(+), 77 deletions(-) create mode 100644 src/routes/account/danger-zone/+page.server.ts create mode 100644 src/routes/account/danger-zone/+page.svelte diff --git a/src/lib/server/admin/user-actions/profile.ts b/src/lib/server/admin/user-actions/profile.ts index d5cbb31..9b0ba52 100644 --- a/src/lib/server/admin/user-actions/profile.ts +++ b/src/lib/server/admin/user-actions/profile.ts @@ -48,7 +48,10 @@ export async function updateProfile(event: UserActionEvent) { // 차단해야 race 우회 가능성을 없앤다 (다른 admin 에게 요청해야 함). // 또한 폼이 전송한 role/status 를 무시하고 DB 현재 값을 그대로 유지한다. let effectiveRole = role; - let effectiveStatus = status; + // status enum 에 deletion_pending(셀프서비스 탈퇴)이 추가되어 users.status 추론 타입이 넓어졌다. + // 자기-자신 편집 시 DB 현재 값(before.status)을 그대로 유지하는데, 그 값이 deletion_pending 일 + // 수도 있으므로 넓은 유니온으로 선언한다(관리자 폼 입력은 위에서 좁게 검증됨). + let effectiveStatus: "active" | "disabled" | "locked" | "deletion_pending" = status; if (userId === locals.user!.id) { if (before && (before.role !== role || before.status !== status)) { return fail(400, { error: adminError(uiLocale, "cannot_change_own_role_status") }); diff --git a/src/lib/server/auth/guards.ts b/src/lib/server/auth/guards.ts index 57014d7..0c41507 100644 --- a/src/lib/server/auth/guards.ts +++ b/src/lib/server/auth/guards.ts @@ -75,7 +75,7 @@ export async function assertUserInTenant( db: DB, tenantId: string, userId: string, -): Promise<{ ok: true; user: { id: string; role: "admin" | "user"; status: "active" | "disabled" | "locked" } } | { ok: false; error: ReturnType }> { +): Promise<{ ok: true; user: { id: string; role: "admin" | "user"; status: "active" | "disabled" | "locked" | "deletion_pending" } } | { ok: false; error: ReturnType }> { const [row] = await db .select({ id: users.id, role: users.role, status: users.status, tenantId: users.tenantId }) .from(users) @@ -84,5 +84,5 @@ export async function assertUserInTenant( if (!row) { return { ok: false, error: fail(404, { error: "사용자를 찾을 수 없습니다." }) }; } - return { ok: true, user: { id: row.id, role: row.role as "admin" | "user", status: row.status as "active" | "disabled" | "locked" } }; + return { ok: true, user: { id: row.id, role: row.role as "admin" | "user", status: row.status as "active" | "disabled" | "locked" | "deletion_pending" } }; } diff --git a/src/lib/server/auth/users.ts b/src/lib/server/auth/users.ts index 817a3cd..6061d3d 100644 --- a/src/lib/server/auth/users.ts +++ b/src/lib/server/auth/users.ts @@ -84,6 +84,44 @@ export async function authenticateLocalUser(db: DB, tenantId: string, username: return user; } +/** + * 탈퇴 신청(soft-delete) 상태 계정의 복구용 재인증. + * + * `authenticateLocalUser` 는 status='active' 만 통과시키므로 deletion_pending 계정은 항상 null 을 + * 반환한다. 로그인 액션에서 그 null 경로 뒤에 이 함수를 호출해 "비밀번호는 맞지만 탈퇴 예정" 인 + * 계정을 식별하고 복구 확인 흐름으로 분기한다. + * + * 타이밍/열거 방어: status 가 deletion_pending 이 아니거나 credential 이 없으면 실제 검증 대신 + * 동일 비용의 더미 scrypt 1회를 태운다. 따라서 이 함수는 어느 경로에서도 정확히 scrypt 1회를 + * 수행하며, authenticateLocalUser 의 null 경로(역시 scrypt 1회)와 합쳐 실패 로그인의 총 비용이 + * 계정 상태와 무관하게 균등해진다(deletion_pending 존재를 타이밍으로 노출하지 않음). + * + * status='deletion_pending' 이고 비밀번호가 일치할 때만 해당 User 를 반환한다. + */ +export async function authenticatePendingDeletionUser(db: DB, tenantId: string, username: string, password: string): Promise { + const user = await findUserByUsername(db, tenantId, username); + + if (!user || user.status !== "deletion_pending") { + await equalizeAuthTiming(password); + return null; + } + + const credential = await findPasswordCredential(db, user.id); + + if (!credential?.secret) { + await equalizeAuthTiming(password); + return null; + } + + const result = await verifyPassword(password, credential.secret); + + if (!result.valid) { + return null; + } + + return user; +} + export async function findActiveUserById(db: DB, userId: string): Promise { const [user] = await db .select() diff --git a/src/lib/server/db/gc.ts b/src/lib/server/db/gc.ts index 81cfd9a..ff3587c 100644 --- a/src/lib/server/db/gc.ts +++ b/src/lib/server/db/gc.ts @@ -21,9 +21,9 @@ * GC 실패는 요청 처리에 절대 영향을 주지 않는다(전부 try/catch + waitUntil 격리). */ -import { lt, or } from "drizzle-orm"; +import { and, eq, lt, or } from "drizzle-orm"; import { getDb, DB_DIALECT, type DB } from "./index"; -import { sessions, oidcGrants, passwordResetTokens, emailVerificationTokens, samlSloStates, samlAuthnRequestIds, samlSessions } from "./schema"; +import { sessions, oidcGrants, passwordResetTokens, emailVerificationTokens, samlSloStates, samlAuthnRequestIds, samlSessions, users } from "./schema"; import { purgeExpiredChallenges } from "$lib/server/auth/webauthn"; import { purgeExpiredRefreshTokens, REFRESH_TOKEN_TTL_MS } from "$lib/server/oidc/refresh"; import { purgeExpiredRateLimits } from "$lib/server/ratelimit"; @@ -179,6 +179,14 @@ export async function runExpiredDataGc(db: DB): Promise { // FK set-null 로 인한 세션 검사 우회가 발생하지 않는다. await runDelete("sessions", () => db.delete(sessions).where(lt(sessions.expiresAt, sessionCutoff))); + // users: 셀프서비스 탈퇴(soft-delete)로 status='deletion_pending' + deletionScheduledAt(유예 만료 + // 30일) 이 지난 계정만 하드 삭제한다. FK onDelete:cascade 가 자식 행(credentials/sessions/…)을 + // 정리하고, audit_events.userId 는 onDelete:set null 로 감사 로그를 보존한다. + // **보수적 조건**: (a) status 가 정확히 deletion_pending 이고 (b) deletionScheduledAt < now 인 + // 행만 대상이다. deletionScheduledAt 이 NULL 인 활성/일반 계정은 `<` 비교에서 참이 되지 않아 + // 절대 매칭되지 않으며(활성 계정 오삭제 불가), 유예 미경과 계정도 삭제되지 않는다. + await runDelete("users", () => db.delete(users).where(and(eq(users.status, "deletion_pending"), lt(users.deletionScheduledAt, now)))); + const result: GcResult = { startedAt, durationMs: Date.now() - startedAt, tables }; const totalDeleted = tables.reduce((sum, t) => sum + (t.deleted ?? 0), 0); diff --git a/src/lib/server/security-notify.ts b/src/lib/server/security-notify.ts index 1bc4fdc..2f2426a 100644 --- a/src/lib/server/security-notify.ts +++ b/src/lib/server/security-notify.ts @@ -12,7 +12,8 @@ export type SecurityEventKind = | "mfa_disabled" | "backup_codes_regenerated" | "passkey_added" - | "passkey_removed"; + | "passkey_removed" + | "account_deletion_requested"; // users.locale ("ko-KR"/"en-US"/"ja-JP" 등) → i18n Locale. en* 만 en, 그 외(미상 포함)는 ko 기본. // 근거: 지원 Locale 은 ko|en 뿐이며(core.ts), 알림 미발송보다 ko 기본 발송이 안전. diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts index c3d396e..bfb0491 100644 --- a/src/routes/(auth)/login/+page.server.ts +++ b/src/routes/(auth)/login/+page.server.ts @@ -3,14 +3,14 @@ import type { Actions, PageServerLoad } from "./$types"; import { getRequestMetadata, recordAuditEvent } from "$lib/server/audit"; import { requireDbContext } from "$lib/server/auth/guards"; import { createSessionRecord, revokeOtherSessions, setSessionCookie } from "$lib/server/auth/session"; -import { authenticateLocalUser, hasTotpCredential, normalizeUsername } from "$lib/server/auth/users"; +import { authenticateLocalUser, authenticatePendingDeletionUser, hasTotpCredential, normalizeUsername } from "$lib/server/auth/users"; import { createMfaPendingToken, MFA_PENDING_COOKIE } from "$lib/server/auth/mfa"; import { AMR_PASSWORD, amrToAcr } from "$lib/server/auth/constants"; import { getRuntimeConfig } from "$lib/server/auth/runtime"; import { checkRateLimit } from "$lib/server/ratelimit"; import { and, eq } from "drizzle-orm"; import type { DB } from "$lib/server/db"; -import { identityProviders, rateLimits } from "$lib/server/db/schema"; +import { identityProviders, rateLimits, users } from "$lib/server/db/schema"; import { authenticateLdap } from "$lib/server/ldap/auth"; import { provisionLdapUser } from "$lib/server/ldap/provision"; import type { LdapProviderConfig } from "$lib/server/ldap/types"; @@ -113,6 +113,7 @@ export const load: PageServerLoad = async ({ locals, url, platform }) => { runtimeError: locals.runtimeError, registered: url.searchParams.get("registered") === "1", passwordReset: url.searchParams.get("passwordReset") === "1", + deletionRequested: url.searchParams.get("deletionRequested") === "1", // OIDC login_hint 전달 시 아이디 입력란 프리필용. loginHint: url.searchParams.get("loginHint")?.trim() || null, }; @@ -124,6 +125,8 @@ export const actions: Actions = { const username = normalizeUsername(String(formData.get("username") ?? "")); const password = String(formData.get("password") ?? ""); const redirectTo = sanitizeRedirectTarget(String(formData.get("redirectTo") ?? "")); + // 탈퇴 예정 계정 복구 확인 흐름의 2단계 제출 표식(1=복구 확정). + const recover = String(formData.get("recover") ?? "") === "1"; const locale = event.locals.locale; if (!username || !password) { @@ -240,6 +243,54 @@ export const actions: Actions = { user = await authenticateLocalUser(db, tenant.id, username, password); } + if (!user) { + // 탈퇴 예정(soft-delete) 계정 복구 흐름. authenticateLocalUser 는 active 만 통과시키므로 + // deletion_pending 계정은 위에서 null 이 된다. 비밀번호가 맞으면(=본인) 복구 확인으로 분기한다. + const pendingUser = await authenticatePendingDeletionUser(db, tenant.id, username, password); + if (pendingUser) { + const nowMs = Date.now(); + const scheduledMs = pendingUser.deletionScheduledAt ? new Date(pendingUser.deletionScheduledAt).getTime() : null; + const withinGrace = scheduledMs !== null && scheduledMs > nowMs; + + if (!withinGrace) { + // 유예 경과 — GC 하드삭제 대상. 복구 불가, 로그인 거부. + await recordAuditEvent(db, { + tenantId: tenant.id, + userId: pendingUser.id, + kind: "login", + outcome: "failure", + ip: requestMetadata.ip, + userAgent: requestMetadata.userAgent, + detail: { username, reason: "deletion_grace_elapsed" }, + }); + const msg = translate(locale, "login.err_account_deleting"); + return fail(400, { username, redirectTo, error: msg, skinHtml: await resolveSkinForAction(event, msg, redirectTo) }); + } + + if (!recover) { + // 1단계: 복구 확인 프롬프트를 띄운다(세션 생성/복구 없음). 사용자가 비밀번호를 + // 다시 입력하고 recover=1 로 재제출하면 아래 2단계에서 실제 복구+로그인이 진행된다. + return { recovery: true as const, username, redirectTo }; + } + + // 2단계: 복구 확정 — 계정을 활성으로 환원하고 삭제 예정을 해제한 뒤 정상 로그인으로 진행. + await db + .update(users) + .set({ status: "active", deletionScheduledAt: null, updatedAt: new Date() }) + .where(and(eq(users.id, pendingUser.id), eq(users.tenantId, tenant.id))); + await recordAuditEvent(db, { + tenantId: tenant.id, + userId: pendingUser.id, + actorId: pendingUser.id, + kind: "user_deletion_cancelled", + outcome: "success", + ip: requestMetadata.ip, + userAgent: requestMetadata.userAgent, + }); + user = { ...pendingUser, status: "active", deletionScheduledAt: null }; + } + } + if (!user) { // 실패 시에만 카운트(성공은 미카운트). 미존재/존재-오답 모두 이 분기를 타므로 // 동일하게 기록되어 열거 오라클을 만들지 않는다. 임계 초과는 다음 요청의 diff --git a/src/routes/(auth)/login/+page.svelte b/src/routes/(auth)/login/+page.svelte index 72091fd..ceecd82 100644 --- a/src/routes/(auth)/login/+page.svelte +++ b/src/routes/(auth)/login/+page.svelte @@ -120,6 +120,12 @@ async function loginWithPasskey() {
{/if} + {#if data.deletionRequested} +
+ {t("login.deletion_requested_notice")} +
+ {/if} + {#if form?.error}
{form.error} @@ -132,82 +138,119 @@ async function loginWithPasskey() {
{/if} - - - -
- - + {#if form?.recovery} + +
+ {t("login.recovery_desc")} +
+ + + + + + +
+ + +
+ + + + + + {:else} +
+ + +
+ + +
+ +
+ + +
-
- - + + + +
+
+ {t("login.or")} +
- -
-
- {t("login.or")} -
-
- - - - + + {/if}
{/if} diff --git a/src/routes/account/danger-zone/+page.server.ts b/src/routes/account/danger-zone/+page.server.ts new file mode 100644 index 0000000..b3474e3 --- /dev/null +++ b/src/routes/account/danger-zone/+page.server.ts @@ -0,0 +1,152 @@ +import { fail, redirect } from "@sveltejs/kit"; +import { and, eq } from "drizzle-orm"; +import type { Actions, PageServerLoad } from "./$types"; +import { getRequestMetadata, recordAuditEvent } from "$lib/server/audit"; +import { requireDbContext, assertNotLastAdmin } from "$lib/server/auth/guards"; +import { clearSessionCookie, revokeAllUserSessions } from "$lib/server/auth/session"; +import { revokeAllUserRefreshTokens } from "$lib/server/oidc/refresh"; +import { findPasswordCredential } from "$lib/server/auth/users"; +import { verifyPassword } from "$lib/server/auth/password"; +import { verifyTotp, decryptTotpSecret, encryptTotpSecret, isLegacyTotpCiphertext } from "$lib/server/auth/totp"; +import { getRuntimeConfig } from "$lib/server/auth/runtime"; +import { tryWithSecrets } from "$lib/server/crypto/keys"; +import { TOTP_CREDENTIAL_TYPE } from "$lib/server/auth/constants"; +import { dispatchSecurityAlert } from "$lib/server/security-notify"; +import { credentials, users } from "$lib/server/db/schema"; +import { translate } from "$lib/i18n/server"; + +// 소프트 삭제 유예기간 = 30일. 이 기간 내 로그인하면 계정을 복구할 수 있고, 경과하면 GC 가 +// 하드 삭제한다. (login 복구 흐름·gc.ts 하드삭제 조건과 짝을 이룬다.) +const DELETION_GRACE_MS = 30 * 24 * 60 * 60 * 1000; + +export const load: PageServerLoad = async ({ locals, url }) => { + if (!locals.user) { + throw redirect(303, `/login?redirectTo=${encodeURIComponent(url.pathname)}`); + } + + const { db } = requireDbContext(locals); + + // step-up UI 구성용: 비밀번호 크레덴셜/ TOTP 보유 여부. + const pwCred = await findPasswordCredential(db, locals.user.id); + const [totpCred] = await db + .select({ id: credentials.id }) + .from(credentials) + .where(and(eq(credentials.userId, locals.user.id), eq(credentials.type, TOTP_CREDENTIAL_TYPE))) + .limit(1); + + return { + email: locals.user.email, + hasPassword: Boolean(pwCred?.secret), + hasTotp: Boolean(totpCred), + graceDays: Math.round(DELETION_GRACE_MS / (24 * 60 * 60 * 1000)), + }; +}; + +export const actions: Actions = { + requestDeletion: async (event) => { + const { locals } = event; + if (!locals.user) throw redirect(303, "/login"); + + const { db, tenant } = requireDbContext(locals); + const locale = locals.locale; + const user = locals.user; + + const formData = await event.request.formData(); + const password = String(formData.get("password") ?? ""); + const totpCode = String(formData.get("totp") ?? "") + .trim() + .replace(/\s/g, ""); + + const requestMetadata = getRequestMetadata(event); + + // ── step-up 재인증 (비밀번호 또는 TOTP) ───────────────────────────────── + // 세션 탈취 공격자가 정당 소유자의 계정을 삭제하지 못하도록 재인증을 강제한다. + // 비밀번호 크레덴셜이 있으면 비밀번호를, 없거나 TOTP 코드를 제출하면 TOTP 를 검증한다. + let stepUpOk = false; + + const pwCred = await findPasswordCredential(db, user.id); + if (password && pwCred?.secret) { + const ok = await verifyPassword(password, pwCred.secret); + stepUpOk = ok.valid; + } + + if (!stepUpOk && totpCode) { + const [totpCred] = await db + .select() + .from(credentials) + .where(and(eq(credentials.userId, user.id), eq(credentials.type, TOTP_CREDENTIAL_TYPE))) + .limit(1); + const config = getRuntimeConfig(event.platform); + if (totpCred?.secret && config.signingKeySecret) { + try { + const plainSecret = await tryWithSecrets(config.signingKeySecrets, (s) => decryptTotpSecret(totpCred.secret!, s, user.id)); + const lastUsedStep = totpCred.counter ?? undefined; + const matchedStep = await verifyTotp(totpCode, plainSecret, lastUsedStep); + if (matchedStep !== null) { + stepUpOk = true; + // v1 형식이면 v2 로 lazy migration + 재사용 방지용 counter 갱신. + let nextSecret = totpCred.secret; + if (isLegacyTotpCiphertext(totpCred.secret)) { + try { + nextSecret = await encryptTotpSecret(plainSecret, config.signingKeySecret, user.id); + } catch { + nextSecret = totpCred.secret; + } + } + await db.update(credentials).set({ lastUsedAt: new Date(), counter: matchedStep, secret: nextSecret }).where(eq(credentials.id, totpCred.id)); + } + } catch { + stepUpOk = false; + } + } + } + + if (!stepUpOk) { + await recordAuditEvent(db, { + tenantId: tenant.id, + userId: user.id, + actorId: user.id, + kind: "user_deletion_stepup_failed", + outcome: "failure", + ip: requestMetadata.ip, + userAgent: requestMetadata.userAgent, + }); + return fail(401, { error: translate(locale, "account.danger_zone.err_reauth") }); + } + + // ── 마지막 활성 관리자 자기삭제 차단 ───────────────────────────────────── + const lastAdminBlock = await assertNotLastAdmin(db, tenant.id, user.id); + if (lastAdminBlock) { + return fail(400, { error: translate(locale, "account.danger_zone.err_last_admin") }); + } + + // ── 소프트 삭제 전환: status=deletion_pending + 유예 30일 예약 ─────────── + const scheduledAt = new Date(Date.now() + DELETION_GRACE_MS); + await db + .update(users) + .set({ status: "deletion_pending", deletionScheduledAt: scheduledAt, updatedAt: new Date() }) + .where(and(eq(users.id, user.id), eq(users.tenantId, tenant.id))); + + // 전 세션 + refresh token 즉시 폐기(로그아웃). 복구는 유예 내 재로그인으로만 가능. + await revokeAllUserSessions(db, user.id); + await revokeAllUserRefreshTokens(db, user.id); + + await recordAuditEvent(db, { + tenantId: tenant.id, + userId: user.id, + actorId: user.id, + kind: "user_deletion_requested", + outcome: "success", + ip: requestMetadata.ip, + userAgent: requestMetadata.userAgent, + detail: { scheduledAt: scheduledAt.toISOString() }, + }); + + // 탈퇴 접수 알림 메일(best-effort). 세션 만료 전, 쿠키 삭제 이전에 발사한다. + dispatchSecurityAlert({ to: user.email, locale: user.locale, kind: "account_deletion_requested", platform: event.platform }); + + // 로그아웃: 세션 쿠키 삭제 후 로그인으로. + clearSessionCookie(event.cookies, event.url); + throw redirect(303, "/login?deletionRequested=1"); + }, +}; diff --git a/src/routes/account/danger-zone/+page.svelte b/src/routes/account/danger-zone/+page.svelte new file mode 100644 index 0000000..26a3188 --- /dev/null +++ b/src/routes/account/danger-zone/+page.svelte @@ -0,0 +1,85 @@ + + +
+
+ + +
+

{t("account.danger_zone.title")}

+

{t("account.danger_zone.subtitle")}

+ + {#if formError} +
+ {formError} +
+ {/if} + +
+

{t("account.danger_zone.warning_title")}

+

{t("account.danger_zone.warning_grace", { days: data.graceDays })}

+

{t("account.danger_zone.warning_recovery")}

+
+ +
+ {#if data.hasPassword} +
+ + +
+ {/if} + + {#if data.hasTotp} +
+ + +
+ {/if} + + + + +
+
+
+
diff --git a/test/unit/gc.test.ts b/test/unit/gc.test.ts index c7ac3c9..d05bd14 100644 --- a/test/unit/gc.test.ts +++ b/test/unit/gc.test.ts @@ -68,6 +68,7 @@ const ALL_TABLES = [ "saml_authn_request_ids", "saml_sessions", "sessions", + "users", ]; describe("runExpiredDataGc — 보수적 만료 조건", () => { @@ -146,6 +147,28 @@ describe("runExpiredDataGc — 보수적 만료 조건", () => { expect(c1).toBeGreaterThanOrEqual(start - SESSION_TTL_MS - 5_000); }); + it("users: status=deletion_pending 이고 deletionScheduledAt 경과분만 삭제한다(활성/미경과 보존)", async () => { + const start = Date.now(); + const { db, deletes } = makeDb(); + await runExpiredDataGc(db); + const end = Date.now(); + + const { sql, params } = whereFor(deletes, "users"); + // 두 조건(AND): status = 'deletion_pending' 그리고 deletion_scheduled_at < now. + expect(sql).toContain('"users"."status" = ?'); + expect(sql).toContain('"users"."deletion_scheduled_at" < ?'); + expect(sql).toContain(" and "); + // deletionScheduledAt 은 오직 `<` 비교로만 쓰인다 → NULL(활성/일반 계정)은 절대 매칭되지 않는다. + expect(sql).not.toContain('"users"."deletion_scheduled_at" is'); + + const [statusParam, cutoff] = params as [string, number]; + expect(statusParam).toBe("deletion_pending"); + // 유예 없이 즉시(now) cutoff — 실제 30일 유예는 신청 시 deletionScheduledAt 에 반영되어 있으므로 + // GC 는 예정 시각 경과분만 지운다. + expect(cutoff).toBeGreaterThanOrEqual(start - 5_000); + expect(cutoff).toBeLessThanOrEqual(end); + }); + it("oidc_grants / password_reset_tokens / saml_slo_states: expiresAt 경과분만 삭제한다", async () => { const { db, deletes } = makeDb(); await runExpiredDataGc(db); From e83b290fba4a75c4c989434b024b589c1e256918 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 13:46:11 +0900 Subject: [PATCH 12/26] =?UTF-8?q?feat(auth):=20P9=20=EC=8B=9C=ED=81=AC?= =?UTF-8?q?=EB=A6=BF=20=EB=AC=B4=EC=A4=91=EB=8B=A8=20=ED=9A=8C=EC=A0=84=20?= =?UTF-8?q?=E2=80=94=20current/previous=20=EC=9D=B4=EC=A4=91=20=EC=8B=9C?= =?UTF-8?q?=ED=81=AC=EB=A6=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RuntimeConfig.signingKeySecrets([current] 또는 [current,previous]): 발급/암호화는 current만, 복호/검증은 tryWithSecrets 로 current→previous 순차 시도 - crypto/keys: tryWithSecrets·tryWithSecretsNullable 헬퍼, getActiveSigningKey(secrets[]) 시그니처 전환, IDP_SIGNING_KEY_SECRET_PREVIOUS 주입 - 소비처 전량 전환: oidc(token/introspect/userinfo/end-session)·saml(sso/slo)·totp/webauthn verify·mfa·logout·login(ldap bindPassword) - scripts/reencrypt-secrets.ts(3종 재암호화)·SECRET_ROTATION.md 갱신, crypto-keys 회전 테스트 - 회전 소비처 프로토콜 라우트는 signingKeySecrets 변경과 잔존 한국어 에러 i18n 이 라인 단위로 얽혀 있어 파일 단위로 본 커밋에 포함(i18n 키는 P10 JSON 에 추가 — 최종 트리에서 정합) Co-Authored-By: Claude Fable 5 --- docs/SECRET_ROTATION.md | 50 ++- scripts/reencrypt-secrets.ts | 299 ++++++++++++++++++ src/lib/server/auth/bootstrap.ts | 14 +- src/lib/server/auth/runtime.ts | 21 +- src/lib/server/crypto/keys.ts | 49 ++- src/routes/(auth)/login/+page.server.ts | 7 +- src/routes/(auth)/logout/+page.server.ts | 6 +- src/routes/(auth)/mfa/+page.server.ts | 7 +- src/routes/account/mfa/+page.server.ts | 9 +- src/routes/api/totp/verify/+server.ts | 8 +- .../api/webauthn/register/verify/+server.ts | 20 +- src/routes/oidc/end-session/+server.ts | 23 +- src/routes/oidc/introspect/+server.ts | 13 +- src/routes/oidc/token/+server.ts | 5 +- src/routes/oidc/userinfo/+server.ts | 21 +- src/routes/saml/slo/+server.ts | 59 ++-- src/routes/saml/sso/+server.ts | 87 ++--- test/unit/crypto-keys.test.ts | 73 +++++ 18 files changed, 626 insertions(+), 145 deletions(-) create mode 100644 scripts/reencrypt-secrets.ts diff --git a/docs/SECRET_ROTATION.md b/docs/SECRET_ROTATION.md index 7d3ac96..6442fd4 100644 --- a/docs/SECRET_ROTATION.md +++ b/docs/SECRET_ROTATION.md @@ -1,7 +1,13 @@ # `IDP_SIGNING_KEY_SECRET` 회전 절차 -> 작성: 2026-07-06 (코드 기준 조사: `src/lib/server/crypto/keys.ts`, `auth/totp.ts`, `auth/mfa.ts`, `auth/webauthn.ts`, `audit/index.ts`). -> 이 시크릿은 단일 마스터 값이며 **키 버전/이중 시크릿 fallback이 없다** — 무중단(zero-downtime) 회전은 현재 코드로는 불가능하고, 아래 절차의 재암호화 창 동안 일부 요청이 실패할 수 있다. +> 작성: 2026-07-06 / 갱신: 2026-07-06 (Phase 9 — 무중단 회전 지원 추가). +> 코드 기준: `src/lib/server/crypto/keys.ts`, `auth/runtime.ts`, `auth/totp.ts`, `auth/mfa.ts`, `auth/webauthn.ts`, `audit/index.ts`. +> +> **무중단(zero-downtime) 회전이 지원된다.** `IDP_SIGNING_KEY_SECRET_PREVIOUS` 에 old +> 시크릿을 병기하면, 모든 복호/검증 경로가 current→previous 순차로 시도(`tryWithSecrets`) +> 하므로 재암호화 창 동안에도 요청이 실패하지 않는다. **발급/암호화(토큰 서명, private key +> 래핑, 시크릿·TOTP 암호화, 쿠키·audit 서명)는 항상 current(`IDP_SIGNING_KEY_SECRET`)만 +> 사용**하므로, 회전 후 새로 쓰이는 데이터는 곧바로 new 시크릿으로 저장된다. ## 1. 이 시크릿이 쓰이는 곳 (7용도) @@ -29,20 +35,38 @@ **C. 선택 후처리** — `audit_events.hash`: 원본 평문이 DB에 있으므로 new secret으로 전량 재계산 가능(전용 스크립트는 현재 없음). 재계산하지 않으면 향후 무결성 검증 시 old-secret 시절 행이 전부 오탐된다. -## 3. 회전 절차 (순서 엄수) +## 3. 무중단 회전 절차 (순서 엄수) -1. **준비**: 새 secret 생성(고엔트로피 256bit 이상). old secret을 재암호화 작업 완료까지 안전하게 보관. 트래픽이 낮은 시간대 선택(재암호화~재배포 사이 실패 창 존재). -2. **재암호화 배치 실행** (전용 스크립트 신규 작성 필요 — 리포에 기성 스크립트 없음): - - 각 테넌트 `signing_keys` 활성 행: `unwrapPrivateKey(old)` → `wrapPrivateKey(new)` 재래핑. (`rotated_at`이 찍힌 과거 행은 코드가 읽지 않으므로 스킵 가능.) - - `credentials` type='totp' 전량: `decryptTotpSecret(old, userId)` → `encryptTotpSecret(new, userId)` (v1 레거시 행은 userId 바인딩 없이 복호됨). - - LDAP `bindPasswordEnc`: `decryptSecret(old, "idp-ldap-bind-password-v1")` → `encryptSecret(new, ...)`. -3. **시크릿 교체·재배포**: `wrangler secret put IDP_SIGNING_KEY_SECRET`(Workers) 또는 환경변수 교체(Node) 후 재배포. **반드시 2단계 완료 후에.** -4. **스모크 테스트**: `/oidc/token` 발급, `/saml/sso` 서명, TOTP 로그인, LDAP 로그인 — 전부 무보호 복호 경로를 타므로 실패 시 즉시 드러난다. JWKS(`/oidc/jwks`)의 kid 정상 노출 확인. -5. **(선택)** `audit_events.hash` 전량 재계산 배치. +핵심: **new 를 current 로 먼저 배포하고 old 를 PREVIOUS 로 병기** → 이 상태에서는 old·new 로 +암호화된 데이터가 모두 복호되고(fallback), 새 데이터는 new 로만 저장된다. 재암호화 후 PREVIOUS +를 제거한다. 어느 시점에도 요청 실패 창이 없다. + +1. **준비**: 새 secret 생성(고엔트로피 256bit 이상). +2. **PREVIOUS 병기 + new 배포** (fallback 활성화): + - `IDP_SIGNING_KEY_SECRET` = **new**, `IDP_SIGNING_KEY_SECRET_PREVIOUS` = **old** 로 설정 후 재배포. + - Workers: `wrangler secret put IDP_SIGNING_KEY_SECRET` / `... IDP_SIGNING_KEY_SECRET_PREVIOUS`. + - Node: 두 환경변수를 설정 후 재시작. + - 이 시점부터 발급/암호화는 new 로, 복호/검증은 new→old 순차로 처리된다(무중단). +3. **재암호화 배치 실행** (`scripts/reencrypt-secrets.ts`): old(PREVIOUS)로 복호 → new 로 재암호화. + - 대상: `signing_keys` 활성 행 `private_jwk_encrypted`, `credentials` type='totp' `secret`, + `identity_providers` kind='ldap' `config_json.bindPasswordEnc`. + - 먼저 **dry-run**(기본): `IDP_SIGNING_KEY_SECRET_PREVIOUS= IDP_SIGNING_KEY_SECRET= DB_DIALECT=... DATABASE_URL=... bun scripts/reencrypt-secrets.ts` + → 대상 건수만 보고, DB 미변경. + - 확인 후 **적용**: 동일 명령에 `--apply` 추가 → DB 쓰기. + - 멱등: 이미 new 로 재암호화된 행은 "already"로 건너뛴다(반복 실행 안전). old/new 둘 다 복호 실패 + 행은 error 로 집계(종료코드 2)하되 배치는 계속 진행한다. + - (프로젝트 규칙상 이 스크립트는 원격 DB 를 변경하므로 자동 실행하지 않는다 — 운영자가 직접 실행.) +4. **스모크 테스트**: `/oidc/token` 발급, `/saml/sso` 서명, TOTP 로그인, LDAP 로그인, JWKS(`/oidc/jwks`) kid 노출 확인. +5. **PREVIOUS 제거로 회전 마무리**: `IDP_SIGNING_KEY_SECRET_PREVIOUS` 삭제 후 재배포. old 로만 복호되던 + 데이터는 3단계에서 모두 new 로 재암호화되었으므로 이제 old 는 불필요하다. 6. **old secret 파기**. +> **`audit_events.hash` 재계산은 이번 범위 밖(수동 절차)**이다. audit hash 는 발급(생성) 전용이라 +> fallback 이 없고, current(new)로 계산된다. 회전 후 무결성 검증 시 old 시절 행이 오탐되지 않게 하려면 +> 원본 평문(모두 DB 에 존재)으로 new 시크릿으로 전량 재계산하는 별도 배치가 필요하다 — 필요 시 수동으로 수행한다. + ## 4. 알려진 한계 / 개선 여지 (별도 트랙) -- 다중 시크릿 fallback(old/new 동시 시도)이 없어 완전 무중단 회전 불가 — 시크릿 버전 태깅 + 순차 재암호화 지원이 근본 해법. -- access-token HMAC·쿠키 서명(#2,#5,#6,#7)이 파생 없이 원문을 공유 — HKDF 도메인 분리로 통일하면 용도별 노출 반경이 줄어든다(단, 교체 시 위 A 항목의 즉시 무효화 특성은 동일). +- access-token HMAC·쿠키 서명(#2,#5,#6,#7)이 파생 없이 원문을 공유 — HKDF 도메인 분리로 통일하면 용도별 노출 반경이 줄어든다. +- `audit_events.hash` 재계산 전용 배치가 아직 없다(위 3절 주석 참조). - admin 콘솔의 "서명키 rotate" 액션은 **현재 secret으로 새 서명키를 만드는 것**이지 이 마스터 시크릿 회전과 무관하다 — 혼동 주의. diff --git a/scripts/reencrypt-secrets.ts b/scripts/reencrypt-secrets.ts new file mode 100644 index 0000000..237705c --- /dev/null +++ b/scripts/reencrypt-secrets.ts @@ -0,0 +1,299 @@ +/** + * 마스터 시크릿(IDP_SIGNING_KEY_SECRET) 무중단 회전용 재암호화 배치. + * + * previous(old) 시크릿으로 저장된 암호문을 current(new) 시크릿으로 재암호화한다. + * Phase 9 무중단 회전 절차의 3단계에 해당한다(자세한 절차: docs/SECRET_ROTATION.md). + * + * 대상 3종: + * 1. signing_keys 활성행(active=true AND rotated_at IS NULL) 의 private_jwk_encrypted + * — HKDF info "idp-signing-key-wrap-v1", AES-256-GCM (salt.iv.ct) + * 2. credentials(type='totp') 의 secret + * — v1: "idp-totp-secret-wrap-v1", v2: "idp-totp-secret-wrap-v2:" + AAD + * (app 의 encrypt/decryptTotpSecret 를 그대로 재사용 — 형식 보존) + * 3. identity_providers(kind='ldap') 의 config_json 내 bindPasswordEnc + * — HKDF info "idp-ldap-bind-password-v1", AES-256-GCM (salt.iv.ct) + * + * ── 사용법 ──────────────────────────────────────────────────────────────────── + * IDP_SIGNING_KEY_SECRET_PREVIOUS='' \ + * IDP_SIGNING_KEY_SECRET='' \ + * DB_DIALECT=postgres DATABASE_URL='...' \ + * bun scripts/reencrypt-secrets.ts # 기본 dry-run (건수만 보고, 미변경) + * + * ... bun scripts/reencrypt-secrets.ts --apply # 실제 적용(DB 쓰기) + * + * ── 안전 특성 ────────────────────────────────────────────────────────────────── + * - 기본이 dry-run 이다. 실제 DB 쓰기는 반드시 `--apply` 를 명시해야 한다. + * - 멱등성: 이미 new 로 재암호화된 행(old 복호 실패 + new 복호 성공)은 "이미 완료"로 + * 간주하고 건너뛴다. 반복 실행해도 안전하다. + * - old/new 둘 다로 복호가 실패하는 행은 error 로 집계하고 계속 진행한다(중단 X). + * - 이 스크립트는 원격 DB 를 변경할 수 있다 — 프로젝트 규칙상 자동 실행 금지. + * 운영자가 값 확인 후 직접 실행해야 한다. + */ +import "reflect-metadata"; +import { and, eq, isNull } from "drizzle-orm"; +import { openScriptDb } from "./lib/db"; +import { decryptTotpSecret, encryptTotpSecret, isLegacyTotpCiphertext } from "../src/lib/server/auth/totp"; + +// ── env ──────────────────────────────────────────────────────────────────────── + +function readEnv(key: string): string | undefined { + const v = process.env[key]; + return v && v.length > 0 ? v : undefined; +} + +// ── AES-256-GCM + HKDF primitives (keys.ts 와 동일 규격: salt16.iv12.ct, SHA-256) ── +// keys.ts 는 $lib alias 를 import 하므로 bun 스크립트에서 직접 import 불가 → +// signing key wrap / generic secret 용 primitive 만 여기 인라인한다(포맷 완전 동일). + +function b64uEncode(bytes: Uint8Array): string { + return btoa(String.fromCharCode(...bytes)) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); +} + +function b64uDecode(str: string): Uint8Array { + const b64 = str.replace(/-/g, "+").replace(/_/g, "/"); + const bin = atob(b64); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); + return arr; +} + +async function deriveGcmKey(secret: string, salt: Uint8Array, info: string, usages: KeyUsage[]): Promise { + const enc = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey("raw", enc.encode(secret), "HKDF", false, ["deriveKey"]); + return crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: salt as BufferSource, info: enc.encode(info) }, keyMaterial, { name: "AES-GCM", length: 256 }, false, usages); +} + +async function decryptGcmBlob(blob: string, secret: string, info: string): Promise { + const parts = blob.split("."); + if (parts.length !== 3) throw new Error("Invalid GCM blob format (expected salt.iv.ct)"); + const [saltB, ivB, ctB] = parts; + const key = await deriveGcmKey(secret, b64uDecode(saltB), info, ["decrypt"]); + const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: b64uDecode(ivB) as BufferSource }, key, b64uDecode(ctB) as BufferSource); + return new Uint8Array(pt); +} + +async function encryptGcmBytes(bytes: Uint8Array, secret: string, info: string): Promise { + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const key = await deriveGcmKey(secret, salt, info, ["encrypt"]); + const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv as BufferSource }, key, bytes as BufferSource); + return `${b64uEncode(salt)}.${b64uEncode(iv)}.${b64uEncode(new Uint8Array(ct))}`; +} + +// ── 재암호화 결과 타입 ─────────────────────────────────────────────────────────── + +type ReencryptResult = { status: "reencrypted"; value: string } | { status: "already" } | { status: "error"; message: string }; + +/** + * generic GCM blob(signing key / LDAP secret) 재암호화. + * old 로 복호 성공 → new 로 재암호화. old 실패 & new 성공 → 이미 완료. 둘 다 실패 → error. + */ +async function reencryptGcmBlob(blob: string, oldSecret: string, newSecret: string, info: string): Promise { + let plaintext: Uint8Array; + try { + plaintext = await decryptGcmBlob(blob, oldSecret, info); + } catch { + try { + await decryptGcmBlob(blob, newSecret, info); + return { status: "already" }; + } catch { + return { status: "error", message: "old/new 둘 다로 복호 실패" }; + } + } + const value = await encryptGcmBytes(plaintext, newSecret, info); + return { status: "reencrypted", value }; +} + +/** TOTP secret 재암호화 (app 의 encrypt/decryptTotpSecret 재사용 — v1/v2 형식 보존). */ +async function reencryptTotpSecret(blob: string, userId: string, oldSecret: string, newSecret: string): Promise { + let plain: string; + try { + plain = await decryptTotpSecret(blob, oldSecret, userId); + } catch { + try { + await decryptTotpSecret(blob, newSecret, userId); + return { status: "already" }; + } catch { + return { status: "error", message: "old/new 둘 다로 복호 실패" }; + } + } + // 형식 보존: 원본이 v1(레거시)이면 v1 으로, v2 면 v2(userId 바인딩)로 재암호화. + const value = isLegacyTotpCiphertext(blob) ? await encryptTotpSecret(plain, newSecret) : await encryptTotpSecret(plain, newSecret, userId); + return { status: "reencrypted", value }; +} + +// ── 집계 카운터 ────────────────────────────────────────────────────────────────── + +interface Counter { + scanned: number; + reencrypted: number; + already: number; + error: number; + skipped: number; // 대상 필드 없음(예: LDAP bindPasswordEnc 미설정) +} + +function newCounter(): Counter { + return { scanned: 0, reencrypted: 0, already: 0, error: 0, skipped: 0 }; +} + +function reportCounter(label: string, c: Counter, apply: boolean): void { + const verb = apply ? "재암호화" : "재암호화 예정"; + console.log(` [${label}] scanned=${c.scanned} ${verb}=${c.reencrypted} already=${c.already} skipped=${c.skipped} error=${c.error}`); +} + +// ── main ───────────────────────────────────────────────────────────────────────── + +async function main(): Promise { + const args = new Set(process.argv.slice(2)); + const apply = args.has("--apply") || args.has("--no-dry-run"); + if (args.has("--dry-run") && apply) { + console.error("✗ --dry-run 과 --apply 를 동시에 지정할 수 없습니다."); + process.exit(1); + } + + const oldSecret = readEnv("IDP_SIGNING_KEY_SECRET_PREVIOUS"); + const newSecret = readEnv("IDP_SIGNING_KEY_SECRET"); + + if (!oldSecret) { + console.error("✗ IDP_SIGNING_KEY_SECRET_PREVIOUS (old 시크릿) 가 필요합니다."); + process.exit(1); + } + if (!newSecret) { + console.error("✗ IDP_SIGNING_KEY_SECRET (new 시크릿) 가 필요합니다."); + process.exit(1); + } + if (oldSecret === newSecret) { + console.error("✗ old 와 new 시크릿이 동일합니다 — 회전할 것이 없습니다."); + process.exit(1); + } + + const h = await openScriptDb(); + const { db, schema } = h; + const { signingKeys, credentials, identityProviders } = schema; + + console.log(`재암호화 배치 (dialect=${h.dialect}, mode=${apply ? "APPLY(DB 쓰기)" : "DRY-RUN(미변경)"})`); + if (!apply) console.log(" ※ dry-run 입니다. 실제 적용하려면 --apply 를 붙이세요."); + + const signing = newCounter(); + const totp = newCounter(); + const ldap = newCounter(); + const errors: string[] = []; + + try { + // ── 1. signing_keys 활성행 private_jwk_encrypted ───────────────────────── + const signingRows = await db + .select({ id: signingKeys.id, tenantId: signingKeys.tenantId, kid: signingKeys.kid, enc: signingKeys.privateJwkEncrypted }) + .from(signingKeys) + .where(and(eq(signingKeys.active, true), isNull(signingKeys.rotatedAt))); + + for (const row of signingRows as Array<{ id: string; tenantId: string; kid: string; enc: string }>) { + signing.scanned++; + const res = await reencryptGcmBlob(row.enc, oldSecret, newSecret, "idp-signing-key-wrap-v1"); + if (res.status === "error") { + signing.error++; + errors.push(`signing_keys id=${row.id} kid=${row.kid}: ${res.message}`); + } else if (res.status === "already") { + signing.already++; + } else { + signing.reencrypted++; + if (apply) { + await db.update(signingKeys).set({ privateJwkEncrypted: res.value }).where(eq(signingKeys.id, row.id)); + } + } + } + + // ── 2. credentials(type='totp') secret ─────────────────────────────────── + const totpRows = await db.select({ id: credentials.id, userId: credentials.userId, secret: credentials.secret }).from(credentials).where(eq(credentials.type, "totp")); + + for (const row of totpRows as Array<{ id: string; userId: string; secret: string | null }>) { + totp.scanned++; + if (!row.secret) { + totp.skipped++; + continue; + } + const res = await reencryptTotpSecret(row.secret, row.userId, oldSecret, newSecret); + if (res.status === "error") { + totp.error++; + errors.push(`credentials(totp) id=${row.id} user=${row.userId}: ${res.message}`); + } else if (res.status === "already") { + totp.already++; + } else { + totp.reencrypted++; + if (apply) { + await db.update(credentials).set({ secret: res.value }).where(eq(credentials.id, row.id)); + } + } + } + + // ── 3. identity_providers(kind='ldap') config_json.bindPasswordEnc ──────── + const ldapRows = await db + .select({ id: identityProviders.id, tenantId: identityProviders.tenantId, configJson: identityProviders.configJson }) + .from(identityProviders) + .where(eq(identityProviders.kind, "ldap")); + + for (const row of ldapRows as Array<{ id: string; tenantId: string; configJson: string | null }>) { + ldap.scanned++; + let cfg: Record; + try { + cfg = JSON.parse(row.configJson ?? "{}") as Record; + } catch { + ldap.error++; + errors.push(`identity_providers(ldap) id=${row.id}: config_json 파싱 실패`); + continue; + } + const enc = typeof cfg.bindPasswordEnc === "string" ? cfg.bindPasswordEnc : null; + if (!enc) { + ldap.skipped++; // 암호화된 bindPassword 가 없는 provider (평문/미설정) + continue; + } + const res = await reencryptGcmBlob(enc, oldSecret, newSecret, "idp-ldap-bind-password-v1"); + if (res.status === "error") { + ldap.error++; + errors.push(`identity_providers(ldap) id=${row.id}: ${res.message}`); + } else if (res.status === "already") { + ldap.already++; + } else { + ldap.reencrypted++; + if (apply) { + const migrated = { ...cfg, bindPasswordEnc: res.value }; + await db + .update(identityProviders) + .set({ configJson: JSON.stringify(migrated), updatedAt: new Date() }) + .where(eq(identityProviders.id, row.id)); + } + } + } + + // ── 결과 요약 ───────────────────────────────────────────────────────────── + console.log("\n── 결과 ──"); + reportCounter("signing_keys", signing, apply); + reportCounter("credentials(totp)", totp, apply); + reportCounter("identity_providers(ldap)", ldap, apply); + + const totalError = signing.error + totp.error + ldap.error; + if (errors.length > 0) { + console.log("\n⚠ 복호 실패 행 (수동 확인 필요):"); + for (const e of errors) console.log(` - ${e}`); + } + + if (!apply) { + console.log("\n✅ dry-run 완료. 실제 적용하려면 동일 env 로 `--apply` 를 붙여 재실행하세요."); + } else { + console.log(`\n✅ 적용 완료. 재암호화된 행: signing=${signing.reencrypted} totp=${totp.reencrypted} ldap=${ldap.reencrypted}.`); + console.log(" 이후 스모크 테스트(OIDC token, SAML SSO, TOTP 로그인, LDAP 로그인) 를 수행하고,"); + console.log(" 확인되면 IDP_SIGNING_KEY_SECRET_PREVIOUS 를 제거해 회전을 마무리하세요."); + } + + if (totalError > 0) process.exit(2); + } finally { + await h.close(); + } +} + +main().catch((err) => { + console.error("✗ 오류:", err instanceof Error ? err.message : err); + process.exit(1); +}); diff --git a/src/lib/server/auth/bootstrap.ts b/src/lib/server/auth/bootstrap.ts index 46d9965..068622d 100644 --- a/src/lib/server/auth/bootstrap.ts +++ b/src/lib/server/auth/bootstrap.ts @@ -4,7 +4,7 @@ import type { DB } from "$lib/server/db"; import { signingKeys, type Tenant, tenants } from "$lib/server/db/schema"; import { DEFAULT_TENANT_SLUG } from "./constants"; import { getRuntimeConfig, type RuntimeConfig } from "./runtime"; -import { generateRsaSigningKey, generateSelfSignedCert, unwrapPrivateKey, wrapPrivateKey } from "$lib/server/crypto/keys"; +import { generateRsaSigningKey, generateSelfSignedCert, tryWithSecrets, unwrapPrivateKey, wrapPrivateKey } from "$lib/server/crypto/keys"; function isUniqueConstraintError(error: unknown): boolean { return error instanceof Error && /unique constraint failed/i.test(error.message); @@ -39,7 +39,7 @@ export async function ensureDefaultTenant(db: DB, platform: App.Platform | undef return tenant; } -export async function ensureSigningKey(db: DB, tenant: Tenant, signingKeySecret: string, issuerUrl?: string): Promise { +export async function ensureSigningKey(db: DB, tenant: Tenant, signingKeySecrets: string[], issuerUrl?: string): Promise { // SAML KeyDescriptor 용 CN let cn = "idp"; if (issuerUrl) { @@ -59,7 +59,8 @@ export async function ensureSigningKey(db: DB, tenant: Tenant, signingKeySecret: // 키가 있지만 cert_pem 이 없는 경우 (M1 → M2 업그레이드): backfill if (existing) { if (!existing.certPem) { - const privateKey = await unwrapPrivateKey(existing.privateJwkEncrypted, signingKeySecret); + // 무보호 예외 지점: 무중단 회전 창에서 previous 로 래핑된 키도 복호되도록 fallback. + const privateKey = await tryWithSecrets(signingKeySecrets, (s) => unwrapPrivateKey(existing.privateJwkEncrypted, s)); const publicJwk = JSON.parse(existing.publicJwk) as JsonWebKey; const publicKey = await crypto.subtle.importKey("jwk", publicJwk, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["verify"]); const certPem = await generateSelfSignedCert(publicKey, privateKey, cn); @@ -69,7 +70,8 @@ export async function ensureSigningKey(db: DB, tenant: Tenant, signingKeySecret: } const { kid, publicKey, privateKey, publicJwk } = await generateRsaSigningKey(); - const privateJwkEncrypted = await wrapPrivateKey(privateKey, signingKeySecret); + // 발급/암호화는 항상 current(=secrets[0])만 사용한다. previous fallback 금지. + const privateJwkEncrypted = await wrapPrivateKey(privateKey, signingKeySecrets[0]); const certPem = await generateSelfSignedCert(publicKey, privateKey, cn); await db.insert(signingKeys).values({ @@ -134,8 +136,8 @@ export async function ensureAuthBaseline(db: DB, platform: App.Platform | undefi // 프로덕션 필수값 검증 — DB 작업 전에 요청 초기에 fail-fast. assertRequiredConfig(config); const tenant = await ensureDefaultTenant(db, platform); - if (config.signingKeySecret) { - await ensureSigningKey(db, tenant, config.signingKeySecret, config.issuerUrl); + if (config.signingKeySecrets.length > 0) { + await ensureSigningKey(db, tenant, config.signingKeySecrets, config.issuerUrl); } g.__idpBaselineCache = { tenant, expiresAt: now + BASELINE_TTL_MS }; diff --git a/src/lib/server/auth/runtime.ts b/src/lib/server/auth/runtime.ts index 848ac48..dfdde20 100644 --- a/src/lib/server/auth/runtime.ts +++ b/src/lib/server/auth/runtime.ts @@ -4,7 +4,18 @@ import { error } from "@sveltejs/kit"; export interface RuntimeConfig { defaultTenantName: string; issuerUrl?: string; + /** + * 마스터 서명/암호화 시크릿의 **current** 값(=`signingKeySecrets[0]`). + * 발급/암호화(토큰 서명, private key 래핑, 시크릿·TOTP 암호화, 쿠키·audit 서명)는 + * **반드시 이 값(current)만** 사용한다. 미설정이면 undefined. + */ signingKeySecret?: string; + /** + * 무중단 회전용 시크릿 목록. `[current]` 또는 `[current, previous]`. + * **복호/검증 경로만** 이 배열을 current→previous 순차로 시도한다(`tryWithSecrets`). + * 미설정이면 빈 배열. `IDP_SIGNING_KEY_SECRET_PREVIOUS` 로 previous 를 주입한다. + */ + signingKeySecrets: string[]; /** * stardust dispatcher 가 idp 의 /api/totp/* 를 호출할 때 사용하는 service token. * Authorization: Bearer 헤더로 검증. 단일 fixed token (rotation 은 수동). @@ -26,10 +37,18 @@ export function getRuntimeConfig(platform: App.Platform | undefined): RuntimeCon const nodeEnv = typeof process !== "undefined" ? (process.env as EnvLookup) : undefined; const getString = (key: string): string | undefined => readString(platformEnv, key) ?? readString(nodeEnv, key); + // 무중단 회전: current(=IDP_SIGNING_KEY_SECRET) 를 [0], previous 를 [1] 로 둔다. + // - current 미설정이면 secrets 는 빈 배열(previous 단독으로는 발급/검증하지 않는다). + // - previous 가 current 와 동일하거나 미설정이면 length 1 → 기존과 동일 동작(회귀 0). + const signingKeyCurrent = getString("IDP_SIGNING_KEY_SECRET"); + const signingKeyPrevious = getString("IDP_SIGNING_KEY_SECRET_PREVIOUS"); + const signingKeySecrets = signingKeyCurrent ? (signingKeyPrevious && signingKeyPrevious !== signingKeyCurrent ? [signingKeyCurrent, signingKeyPrevious] : [signingKeyCurrent]) : []; + return { defaultTenantName: getString("IDP_DEFAULT_TENANT_NAME") ?? "Default Tenant", issuerUrl: getString("IDP_ISSUER_URL")?.trim().replace(/\/$/, ""), - signingKeySecret: getString("IDP_SIGNING_KEY_SECRET"), + signingKeySecret: signingKeyCurrent, + signingKeySecrets, dispatcherServiceToken: getString("DISPATCHER_SERVICE_TOKEN"), }; } diff --git a/src/lib/server/crypto/keys.ts b/src/lib/server/crypto/keys.ts index c38eaea..d6e92f1 100644 --- a/src/lib/server/crypto/keys.ts +++ b/src/lib/server/crypto/keys.ts @@ -32,6 +32,48 @@ export function b64uDecode(str: string): Uint8Array { return arr; } +// ── 무중단 시크릿 회전 헬퍼 ──────────────────────────────────────────────────── + +/** + * 마스터 시크릿 무중단 회전 지원 헬퍼 (Phase 9). + * + * `secrets` 를 순서대로(current=[0] → previous=[1]) 시도하며, `fn` 이 성공하면 그 + * 결과를 반환한다. 전부 실패(throw)하면 **마지막** 에러를 다시 throw 한다. + * + * - **복호/검증 경로 전용**이다. 발급/암호화는 절대 fallback 하지 않고 항상 + * `secrets[0]`(current) 만 사용해야 한다. + * - throw 로 실패를 알리는 함수(`unwrapPrivateKey`, `decryptSecret`, + * `decryptTotpSecret` 등)에 사용한다. null 로 실패를 알리는 검증 함수는 + * `tryWithSecretsNullable` 를 사용한다. + */ +export async function tryWithSecrets(secrets: string[], fn: (secret: string) => Promise): Promise { + if (secrets.length === 0) throw new Error("tryWithSecrets: 시크릿이 설정되지 않았습니다."); + let lastError: unknown; + for (const secret of secrets) { + try { + return await fn(secret); + } catch (error) { + lastError = error; + } + } + throw lastError; +} + +/** + * `tryWithSecrets` 의 null-반환 검증 함수용 변형. + * + * `verifyAccessToken` / `verifyMfaPendingToken` / `verifyChallengeCookie` 처럼 + * 실패 시 throw 대신 `null` 을 반환하는 함수에 사용한다. current→previous 순차로 + * 시도해 **최초의 non-null** 결과를 반환하고, 전부 null 이면 null 을 반환한다. + */ +export async function tryWithSecretsNullable(secrets: string[], fn: (secret: string) => Promise): Promise { + for (const secret of secrets) { + const result = await fn(secret); + if (result !== null) return result; + } + return null; +} + // ── private key wrapping ────────────────────────────────────────────────────── async function deriveWrappingKey(secret: string, salt: Uint8Array): Promise { @@ -335,7 +377,7 @@ export function invalidateSigningKeyCache(tenantId: string): void { publicJwksCache().delete(tenantId); } -export async function getActiveSigningKey(db: DB, tenantId: string, secret: string): Promise { +export async function getActiveSigningKey(db: DB, tenantId: string, secrets: string[]): Promise { const cache = activeSigningKeyCache(); const now = Date.now(); const hit = cache.get(tenantId); @@ -347,7 +389,10 @@ export async function getActiveSigningKey(db: DB, tenantId: string, secret: stri .where(and(eq(signingKeys.tenantId, tenantId), eq(signingKeys.active, true), isNull(signingKeys.rotatedAt))) .limit(1); if (!row) return null; // 활성 키 없음 → 캐시하지 않는다(키 생성 직후 즉시 반영). - const privateKey = await unwrapPrivateKey(row.privateJwkEncrypted, secret); + // 무중단 회전: 저장된 private key 가 previous 시크릿으로 래핑돼 있을 수 있으므로 + // current→previous 순차로 unwrap 을 시도한다. (서명 자체는 RSA 키로 하므로 발급 + // "current 고정" 원칙과 무관 — 여기선 저장 키 복호에만 fallback 을 적용한다.) + const privateKey = await tryWithSecrets(secrets, (s) => unwrapPrivateKey(row.privateJwkEncrypted, s)); const value: ActiveSigningKeyValue = { kid: row.kid, privateKey, diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts index bfb0491..640a9fa 100644 --- a/src/routes/(auth)/login/+page.server.ts +++ b/src/routes/(auth)/login/+page.server.ts @@ -14,7 +14,7 @@ import { identityProviders, rateLimits, users } from "$lib/server/db/schema"; import { authenticateLdap } from "$lib/server/ldap/auth"; import { provisionLdapUser } from "$lib/server/ldap/provision"; import type { LdapProviderConfig } from "$lib/server/ldap/types"; -import { decryptSecret, encryptSecret } from "$lib/server/crypto/keys"; +import { decryptSecret, encryptSecret, tryWithSecrets } from "$lib/server/crypto/keys"; import { resolveSkinHtml, replacePlaceholders, escapeHtml } from "$lib/server/skin/resolver"; import { sanitizeRedirectTarget } from "$lib/server/auth/redirect"; import { translate } from "$lib/i18n/server"; @@ -202,9 +202,10 @@ export const actions: Actions = { // 암호화된 bindPassword 가 있으면 복호화 (레거시 평문 bindPassword 는 그대로 사용) const config = getRuntimeConfig(event.platform); - if (ldapConfig.bindPasswordEnc && !ldapConfig.bindPassword && config.signingKeySecret) { + if (ldapConfig.bindPasswordEnc && !ldapConfig.bindPassword && config.signingKeySecrets.length > 0) { try { - ldapConfig.bindPassword = await decryptSecret(ldapConfig.bindPasswordEnc, config.signingKeySecret, "idp-ldap-bind-password-v1"); + // 무중단 회전: previous 로 암호화된 bindPassword 도 복호되도록 fallback. + ldapConfig.bindPassword = await tryWithSecrets(config.signingKeySecrets, (s) => decryptSecret(ldapConfig.bindPasswordEnc!, s, "idp-ldap-bind-password-v1")); } catch { // 복호화 실패 시 인증 진행 불가 — bindPassword 없이 진행하면 null 반환됨 } diff --git a/src/routes/(auth)/logout/+page.server.ts b/src/routes/(auth)/logout/+page.server.ts index b335151..498da2a 100644 --- a/src/routes/(auth)/logout/+page.server.ts +++ b/src/routes/(auth)/logout/+page.server.ts @@ -43,11 +43,11 @@ async function performLogout(event: RequestEvent): Promise { // OIDC back-channel 로그아웃 발송 (waitUntil) const issuerUrl = resolveIssuerUrl(event.locals.runtimeConfig, event.url.origin); - const signingKeySecret = event.locals.runtimeConfig.signingKeySecret; - if (signingKeySecret) { + const signingKeySecrets = event.locals.runtimeConfig.signingKeySecrets; + if (signingKeySecrets.length > 0) { const bcTargets = await getOidcBackchannelTargets(db, tenant.id, sessionId); if (bcTargets.length > 0) { - const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecret); + const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecrets); if (signingKey) { const bcPromises = bcTargets.map((t) => sendOneBackchannelLogout(t, userId, idpSessionId, issuerUrl, signingKey.privateKey, signingKey.kid).catch(() => undefined)); const wait = event.platform?.ctx?.waitUntil?.bind(event.platform.ctx); diff --git a/src/routes/(auth)/mfa/+page.server.ts b/src/routes/(auth)/mfa/+page.server.ts index 7cc92be..c6c9482 100644 --- a/src/routes/(auth)/mfa/+page.server.ts +++ b/src/routes/(auth)/mfa/+page.server.ts @@ -5,6 +5,7 @@ import { getRequestMetadata, recordAuditEvent } from "$lib/server/audit"; import { requireDbContext } from "$lib/server/auth/guards"; import { createSessionRecord, revokeOtherSessions, setSessionCookie } from "$lib/server/auth/session"; import { verifyMfaPendingToken, MFA_PENDING_COOKIE } from "$lib/server/auth/mfa"; +import { tryWithSecrets, tryWithSecretsNullable } from "$lib/server/crypto/keys"; import { verifyTotp, decryptTotpSecret, encryptTotpSecret, isLegacyTotpCiphertext, verifyBackupCode } from "$lib/server/auth/totp"; import { checkRateLimit } from "$lib/server/ratelimit"; import { AMR_PASSWORD, AMR_TOTP, AMR_BACKUP_CODE, amrToAcr, TOTP_CREDENTIAL_TYPE, BACKUP_CODE_CREDENTIAL_TYPE } from "$lib/server/auth/constants"; @@ -30,7 +31,7 @@ export const load: PageServerLoad = async ({ locals, cookies, platform, url }) = throw redirect(303, "/login"); } - const claims = await verifyMfaPendingToken(mfaToken, config.signingKeySecret); + const claims = await tryWithSecretsNullable(config.signingKeySecrets, (s) => verifyMfaPendingToken(mfaToken, s)); if (!claims) { cookies.delete(MFA_PENDING_COOKIE, { path: "/" }); throw redirect(303, "/login"); @@ -94,7 +95,7 @@ export const actions: Actions = { return fail(503, { error: msg, skinHtml: await resolveMfaSkinForAction(event, msg) }); } - const claims = await verifyMfaPendingToken(mfaToken, config.signingKeySecret); + const claims = await tryWithSecretsNullable(config.signingKeySecrets, (s) => verifyMfaPendingToken(mfaToken, s)); if (!claims) { event.cookies.delete(MFA_PENDING_COOKIE, { path: "/" }); throw redirect(303, "/login"); @@ -173,7 +174,7 @@ export const actions: Actions = { .limit(1); if (totpCred?.secret) { - const plainSecret = await decryptTotpSecret(totpCred.secret, config.signingKeySecret, user.id); + const plainSecret = await tryWithSecrets(config.signingKeySecrets, (s) => decryptTotpSecret(totpCred.secret!, s, user.id)); // counter 컬럼을 마지막으로 사용된 TOTP 스텝으로 활용 (재사용 방지) const lastUsedStep = totpCred.counter ?? undefined; const matchedStep = await verifyTotp(code, plainSecret, lastUsedStep); diff --git a/src/routes/account/mfa/+page.server.ts b/src/routes/account/mfa/+page.server.ts index 90776b3..ab34582 100644 --- a/src/routes/account/mfa/+page.server.ts +++ b/src/routes/account/mfa/+page.server.ts @@ -4,6 +4,7 @@ import type { Actions, PageServerLoad } from "./$types"; import { requireDbContext } from "$lib/server/auth/guards"; import { getRuntimeConfig } from "$lib/server/auth/runtime"; import { generateTotpSecret, buildOtpAuthUri, verifyTotp, encryptTotpSecret, decryptTotpSecret, generateBackupCodes, hashBackupCode } from "$lib/server/auth/totp"; +import { tryWithSecrets, tryWithSecretsNullable } from "$lib/server/crypto/keys"; import { TOTP_CREDENTIAL_TYPE, BACKUP_CODE_CREDENTIAL_TYPE } from "$lib/server/auth/constants"; import { credentials } from "$lib/server/db/schema"; import { recordAuditEvent, getRequestMetadata } from "$lib/server/audit"; @@ -83,7 +84,7 @@ export const load: PageServerLoad = async ({ locals, cookies, platform, url }) = if (!totpCred && config.signingKeySecret) { const setupToken = cookies.get(TOTP_SETUP_COOKIE); if (setupToken) { - const secret = await verifySetupToken(setupToken, config.signingKeySecret); + const secret = await tryWithSecretsNullable(config.signingKeySecrets, (s) => verifySetupToken(setupToken, s)); if (secret) { const issuer = config.issuerUrl ? new URL(config.issuerUrl).hostname : "IdP"; pendingUri = buildOtpAuthUri(secret, locals.user.email, issuer); @@ -151,7 +152,7 @@ export const actions: Actions = { }); } - const plainSecret = await verifySetupToken(setupToken, config.signingKeySecret); + const plainSecret = await tryWithSecretsNullable(config.signingKeySecrets, (s) => verifySetupToken(setupToken, s)); if (!plainSecret) { cookies.delete(TOTP_SETUP_COOKIE, { path: "/" }); return fail(400, { @@ -261,7 +262,7 @@ export const actions: Actions = { return fail(400, { delete: true, error: "TOTP 인증기가 등록되어 있지 않습니다." }); } - const plainSecret = await decryptTotpSecret(totpCred.secret, config.signingKeySecret, locals.user.id); + const plainSecret = await tryWithSecrets(config.signingKeySecrets, (s) => decryptTotpSecret(totpCred.secret!, s, locals.user!.id)); const matchedStep = await verifyTotp(code, plainSecret); if (matchedStep === null) { return fail(400, { delete: true, error: "인증 코드가 올바르지 않습니다." }); @@ -319,7 +320,7 @@ export const actions: Actions = { return fail(400, { regenerate: true, error: "TOTP 인증기가 등록되어 있지 않습니다." }); } - const plainSecret = await decryptTotpSecret(totpCred.secret, config.signingKeySecret, locals.user.id); + const plainSecret = await tryWithSecrets(config.signingKeySecrets, (s) => decryptTotpSecret(totpCred.secret!, s, locals.user!.id)); const matchedStep = await verifyTotp(code, plainSecret); if (matchedStep === null) { return fail(400, { regenerate: true, error: "인증 코드가 올바르지 않습니다." }); diff --git a/src/routes/api/totp/verify/+server.ts b/src/routes/api/totp/verify/+server.ts index c0d1431..068d972 100644 --- a/src/routes/api/totp/verify/+server.ts +++ b/src/routes/api/totp/verify/+server.ts @@ -4,8 +4,10 @@ import { requireServiceToken } from "$lib/server/auth/service-token"; import { TOTP_CREDENTIAL_TYPE } from "$lib/server/auth/constants"; import { requireDbContext } from "$lib/server/auth/guards"; import { decryptTotpSecret, verifyTotp } from "$lib/server/auth/totp"; +import { tryWithSecrets } from "$lib/server/crypto/keys"; import { checkRateLimit } from "$lib/server/ratelimit"; import { credentials } from "$lib/server/db/schema"; +import { translate } from "$lib/i18n/server"; /** * Phase 7.3 — TOTP step-up 검증. @@ -18,7 +20,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { const config = locals.runtimeConfig; if (!config.signingKeySecret) { - throw error(503, "IDP_SIGNING_KEY_SECRET 미설정"); + throw error(503, translate(locals.locale, "totp.errors.signing_key_not_set")); } const body = (await request.json().catch(() => null)) as { userId?: string; code?: string } | null; @@ -31,7 +33,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { // (5분 창에 10회 — webauthn-verify 와 동일 강도.) const rl = await checkRateLimit(db, `totp-verify:${userId}`, { windowMs: 5 * 60 * 1000, limit: 10 }); if (!rl.allowed) { - throw error(429, "TOTP 시도가 너무 많습니다. 잠시 후 다시 시도해 주세요."); + throw error(429, translate(locals.locale, "totp.errors.verify_rate_limited")); } const [cred] = await db @@ -41,7 +43,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { .limit(1); if (!cred || !cred.secret) throw error(404, "TOTP not enrolled"); - const plain = await decryptTotpSecret(cred.secret, config.signingKeySecret, userId); + const plain = await tryWithSecrets(config.signingKeySecrets, (s) => decryptTotpSecret(cred.secret!, s, userId)); // ctrls C3: counter 컬럼을 마지막 사용 스텝으로 활용해 코드 재사용을 거부한다 // (웹 (auth)/mfa 경로와 동일 정책). const lastUsedStep = cred.counter ?? undefined; diff --git a/src/routes/api/webauthn/register/verify/+server.ts b/src/routes/api/webauthn/register/verify/+server.ts index 6db57e3..ddd17b4 100644 --- a/src/routes/api/webauthn/register/verify/+server.ts +++ b/src/routes/api/webauthn/register/verify/+server.ts @@ -7,6 +7,8 @@ import { dispatchSecurityAlert } from "$lib/server/security-notify"; import { checkRateLimit } from "$lib/server/ratelimit"; import { verifyChallengeCookie, verifyRegistrationResponse, savePasskey, getWebAuthnConfig, WEBAUTHN_CHALLENGE_COOKIE } from "$lib/server/auth/webauthn"; import type { RegistrationResponseJSON } from "$lib/server/auth/webauthn"; +import { tryWithSecretsNullable } from "$lib/server/crypto/keys"; +import { translate } from "$lib/i18n/server"; /** * 패스키 라벨에서 제어문자/BIDI override 등 위험 코드포인트를 제거한다. @@ -30,7 +32,7 @@ function sanitizePasskeyLabel(label: string): string { export const POST: RequestHandler = async (event) => { const { locals, cookies, request, url, platform } = event; if (!locals.user) { - throw error(401, "로그인이 필요합니다."); + throw error(401, translate(locals.locale, "webauthn.errors.login_required")); } const { rpID, origin } = getWebAuthnConfig(url); @@ -38,23 +40,23 @@ export const POST: RequestHandler = async (event) => { // Origin 검증 const reqOrigin = request.headers.get("origin"); if (reqOrigin && reqOrigin !== origin) { - throw error(403, "유효하지 않은 출처입니다."); + throw error(403, translate(locals.locale, "webauthn.errors.invalid_origin")); } const config = getRuntimeConfig(platform); - if (!config.signingKeySecret) { - throw error(503, "IDP_SIGNING_KEY_SECRET 이 설정되지 않았습니다."); + if (config.signingKeySecrets.length === 0) { + throw error(503, translate(locals.locale, "webauthn.errors.signing_key_not_configured")); } const cookieValue = cookies.get(WEBAUTHN_CHALLENGE_COOKIE); if (!cookieValue) { - throw error(400, "등록 세션이 만료되었습니다. 다시 시도해 주세요."); + throw error(400, translate(locals.locale, "webauthn.errors.register_session_expired")); } - const payload = await verifyChallengeCookie(cookieValue, config.signingKeySecret, "register"); + const payload = await tryWithSecretsNullable(config.signingKeySecrets, (s) => verifyChallengeCookie(cookieValue, s, "register")); if (!payload || payload.userId !== locals.user.id) { cookies.delete(WEBAUTHN_CHALLENGE_COOKIE, { path: "/" }); - throw error(400, "등록 세션이 유효하지 않습니다. 다시 시도해 주세요."); + throw error(400, translate(locals.locale, "webauthn.errors.register_session_invalid")); } const body = (await request.json()) as RegistrationResponseJSON & { label?: string }; @@ -65,7 +67,7 @@ export const POST: RequestHandler = async (event) => { const { ipKey } = getRequestMetadata(event); const rl = await checkRateLimit(dbForRl, `webauthn-register-verify:${tenantForRl.id}:${ipKey}`, { windowMs: 5 * 60 * 1000, limit: 10 }); if (!rl.allowed) { - throw error(429, "등록 시도가 너무 많습니다. 잠시 후 다시 시도해 주세요."); + throw error(429, translate(locals.locale, "webauthn.errors.register_rate_limited")); } const verification = await verifyRegistrationResponse({ @@ -78,7 +80,7 @@ export const POST: RequestHandler = async (event) => { cookies.delete(WEBAUTHN_CHALLENGE_COOKIE, { path: "/" }); if (!verification.verified || !verification.registrationInfo) { - throw error(400, "패스키 등록 검증에 실패했습니다."); + throw error(400, translate(locals.locale, "webauthn.errors.register_verify_failed")); } const { db, tenant } = requireDbContext(locals); diff --git a/src/routes/oidc/end-session/+server.ts b/src/routes/oidc/end-session/+server.ts index 4bcf2cc..c545ce2 100644 --- a/src/routes/oidc/end-session/+server.ts +++ b/src/routes/oidc/end-session/+server.ts @@ -7,6 +7,8 @@ import { getActiveSigningKey, verifyIdToken } from "$lib/server/crypto/keys"; import { getOidcBackchannelTargets, getOidcFrontchannelTargets, sendOneBackchannelLogout } from "$lib/server/oidc/logout"; import { matchesRedirectUri } from "$lib/server/oidc/client"; import { resolveIssuerUrl } from "$lib/server/auth/runtime"; +import { translate } from "$lib/i18n/server"; +import type { Locale } from "$lib/i18n/core"; function htmlEscape(s: string): string { return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); @@ -25,7 +27,7 @@ function isSafeRedirectScheme(url: string): boolean { } } -function renderFrontchannelLogoutHtml(iframeUris: string[], redirectTo: string): string { +function renderFrontchannelLogoutHtml(iframeUris: string[], redirectTo: string, locale: Locale): string { // ctrls H-OIDC-6: iframe sandbox 강화. // - sandbox="" (모든 권한 제거) → RP iframe 안에서 script/popup/topnav 전부 차단. // 기존 allow-scripts 는 RP frontchannel logout 표준 (script 로 RP 측 세션 정리) @@ -37,11 +39,11 @@ function renderFrontchannelLogoutHtml(iframeUris: string[], redirectTo: string): const safeRedirect = isSafeRedirectScheme(redirectTo) ? redirectTo : "/"; // CSP 는 hash 모드이므로 inline JS 를 피하고 meta refresh 를 사용한다. return ( - `` + + `` + `Logging out...` + `` + `` + - `

로그아웃 중...

` + + `

${htmlEscape(translate(locale, "oidc.errors.logging_out"))}

` + iframes + `` ); @@ -84,7 +86,7 @@ export const GET: RequestHandler = async (event) => { // ctrls M-10: id_token_hint 가 없으면 거부 (CSRF / drive-by logout 방지) if (!idTokenHint) { - return new Response(JSON.stringify({ error: "invalid_request", error_description: "id_token_hint 가 필요합니다." }), { + return new Response(JSON.stringify({ error: "invalid_request", error_description: translate(locals.locale, "oidc.errors.id_token_hint_required") }), { status: 400, headers: { "Content-Type": "application/json" }, }); @@ -117,7 +119,7 @@ export const GET: RequestHandler = async (event) => { const aud = claims.aud; const audMatches = typeof aud === "string" ? aud === clientId : Array.isArray(aud) ? aud.includes(clientId) : false; if (!audMatches) { - return new Response(JSON.stringify({ error: "invalid_id_token_hint", error_description: "aud mismatch" }), { + return new Response(JSON.stringify({ error: "invalid_id_token_hint", error_description: translate(locals.locale, "oidc.errors.aud_mismatch") }), { status: 400, headers: { "Content-Type": "application/json" }, }); @@ -166,13 +168,13 @@ async function executeLogout(event: Parameters[0], postLogoutRed const userId = locals.user.id; const issuerUrl = resolveIssuerUrl(locals.runtimeConfig, url.origin); - const signingKeySecret = locals.runtimeConfig.signingKeySecret; + const signingKeySecrets = locals.runtimeConfig.signingKeySecrets; const bcTargets = await getOidcBackchannelTargets(db, tenantId, sessionId); const fcTargets = await getOidcFrontchannelTargets(db, tenantId, sessionId, idpSessionId, issuerUrl); - if (bcTargets.length > 0 && signingKeySecret) { - const signingKey = await getActiveSigningKey(db, tenantId, signingKeySecret); + if (bcTargets.length > 0 && signingKeySecrets.length > 0) { + const signingKey = await getActiveSigningKey(db, tenantId, signingKeySecrets); if (signingKey) { const bcPromises = bcTargets.map((t) => sendOneBackchannelLogout(t, userId, idpSessionId, issuerUrl, signingKey.privateKey, signingKey.kid).catch(() => undefined)); const wait = platform?.ctx?.waitUntil?.bind(platform.ctx); @@ -192,6 +194,7 @@ async function executeLogout(event: Parameters[0], postLogoutRed const html = renderFrontchannelLogoutHtml( fcTargets.map((t) => t.uri), redirectTo, + locals.locale, ); return new Response(html, { status: 200, @@ -224,7 +227,7 @@ export const POST: RequestHandler = async (event) => { } }; if (!sameOrigin(origin) && !sameOrigin(referer)) { - return new Response(JSON.stringify({ error: "invalid_request", error_description: "cross-origin POST 차단" }), { + return new Response(JSON.stringify({ error: "invalid_request", error_description: translate(locals.locale, "oidc.errors.cross_origin_post_blocked") }), { status: 403, headers: { "Content-Type": "application/json" }, }); @@ -259,7 +262,7 @@ export const POST: RequestHandler = async (event) => { const aud = claims.aud; const audMatches = typeof aud === "string" ? aud === clientId : Array.isArray(aud) ? aud.includes(clientId) : false; if (!audMatches) { - return new Response(JSON.stringify({ error: "invalid_id_token_hint", error_description: "aud mismatch" }), { + return new Response(JSON.stringify({ error: "invalid_id_token_hint", error_description: translate(locals.locale, "oidc.errors.aud_mismatch") }), { status: 400, headers: { "Content-Type": "application/json" }, }); diff --git a/src/routes/oidc/introspect/+server.ts b/src/routes/oidc/introspect/+server.ts index b71efc8..12f286a 100644 --- a/src/routes/oidc/introspect/+server.ts +++ b/src/routes/oidc/introspect/+server.ts @@ -16,7 +16,8 @@ import { getRequestMetadata } from "$lib/server/audit"; import { checkRateLimit } from "$lib/server/ratelimit"; import { authenticateOidcClient } from "$lib/server/oidc/client"; import { findActiveRefreshToken } from "$lib/server/oidc/refresh"; -import { verifyAccessToken } from "$lib/server/crypto/keys"; +import { verifyAccessToken, tryWithSecretsNullable } from "$lib/server/crypto/keys"; +import { translate } from "$lib/i18n/server"; function errorResponse(code: string, description: string, status: number): Response { return new Response(JSON.stringify({ error: code, error_description: description }), { @@ -34,12 +35,12 @@ function inactive(): Response { export const POST: RequestHandler = async (event) => { const { locals, request } = event; const { db, tenant } = requireDbContext(locals); - const { signingKeySecret } = locals.runtimeConfig; + const { signingKeySecrets } = locals.runtimeConfig; const { ipKey } = getRequestMetadata(event); const rl = await checkRateLimit(db, `oidc-introspect:${ipKey}`, { windowMs: 60 * 1000, limit: 60 }); if (!rl.allowed) { - return new Response(JSON.stringify({ error: "rate_limit_exceeded", error_description: "요청이 너무 많습니다." }), { + return new Response(JSON.stringify({ error: "rate_limit_exceeded", error_description: translate(locals.locale, "oidc.errors.rate_limited_short") }), { status: 429, headers: { "Content-Type": "application/json", "Retry-After": String(Math.ceil(rl.retryAfterMs / 1000)) }, }); @@ -50,14 +51,14 @@ export const POST: RequestHandler = async (event) => { if (!auth.ok) return errorResponse(auth.code, auth.description, auth.status); const token = String(body.get("token") ?? ""); - if (!token) return errorResponse("invalid_request", "token 파라미터가 필요합니다.", 400); + if (!token) return errorResponse("invalid_request", translate(locals.locale, "oidc.errors.token_param_required"), 400); const hint = String(body.get("token_type_hint") ?? ""); const clientId = auth.client.clientId; // 1) access token 시도 (refresh_token 힌트가 명시된 경우는 건너뜀). - if (hint !== "refresh_token" && signingKeySecret) { - const claims = await verifyAccessToken(token, signingKeySecret, tenant.id, clientId); + if (hint !== "refresh_token" && signingKeySecrets.length > 0) { + const claims = await tryWithSecretsNullable(signingKeySecrets, (s) => verifyAccessToken(token, s, tenant.id, clientId)); if (claims) { return json( { diff --git a/src/routes/oidc/token/+server.ts b/src/routes/oidc/token/+server.ts index a3bc4c5..be103c8 100644 --- a/src/routes/oidc/token/+server.ts +++ b/src/routes/oidc/token/+server.ts @@ -184,7 +184,8 @@ function tokenResponse(body: Record): Response { export const POST: RequestHandler = async (event) => { const { locals, request, url } = event; const { db, tenant } = requireDbContext(locals); - const { signingKeySecret } = locals.runtimeConfig; + // signingKeySecret = current (발급 전용), signingKeySecrets = 복호 fallback 용. + const { signingKeySecret, signingKeySecrets } = locals.runtimeConfig; // 레이트 리밋: IP당 30회/분 const { ip, ipKey, userAgent } = getRequestMetadata(event); @@ -302,7 +303,7 @@ export const POST: RequestHandler = async (event) => { return tokenError("unauthorized_client", "이 클라이언트에 허용되지 않은 grant_type 입니다."); } - const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecret); + const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecrets); if (!signingKey) { return tokenError("server_error", "활성 서명 키를 찾을 수 없습니다.", 503); } diff --git a/src/routes/oidc/userinfo/+server.ts b/src/routes/oidc/userinfo/+server.ts index f71536b..9a1128b 100644 --- a/src/routes/oidc/userinfo/+server.ts +++ b/src/routes/oidc/userinfo/+server.ts @@ -3,10 +3,11 @@ import type { RequestHandler } from "./$types"; import { and, eq } from "drizzle-orm"; import { requireDbContext } from "$lib/server/auth/guards"; import { oidcClients, users } from "$lib/server/db/schema"; -import { verifyAccessToken } from "$lib/server/crypto/keys"; +import { verifyAccessToken, tryWithSecretsNullable } from "$lib/server/crypto/keys"; import { getUserMembership, membershipToGroups } from "$lib/server/org/membership"; import { getActiveAssignment, parseAssignmentAttributes } from "$lib/server/access/service-permissions"; import { buildAddressClaim } from "$lib/server/oidc/claims"; +import { translate } from "$lib/i18n/server"; const RESERVED_USERINFO_CLAIMS = new Set(["sub", "iss", "aud", "iat", "exp", "auth_time"]); @@ -22,29 +23,29 @@ function bearerError(code: string, description: string): Response { async function handleUserinfo(locals: App.Locals, request: Request): Promise { const { db, tenant } = requireDbContext(locals); - const { signingKeySecret } = locals.runtimeConfig; + const { signingKeySecrets } = locals.runtimeConfig; - if (!signingKeySecret) { - return new Response("IDP_SIGNING_KEY_SECRET 미설정", { status: 503 }); + if (signingKeySecrets.length === 0) { + return new Response(translate(locals.locale, "oidc.errors.signing_key_not_set"), { status: 503 }); } const authHeader = request.headers.get("Authorization"); if (!authHeader?.startsWith("Bearer ")) { - return bearerError("invalid_token", "Bearer 토큰이 필요합니다."); + return bearerError("invalid_token", translate(locals.locale, "oidc.errors.bearer_token_required")); } const token = authHeader.slice(7); - const claims = await verifyAccessToken(token, signingKeySecret, tenant.id); + const claims = await tryWithSecretsNullable(signingKeySecrets, (s) => verifyAccessToken(token, s, tenant.id)); if (!claims) { - return bearerError("invalid_token", "유효하지 않거나 만료된 액세스 토큰입니다."); + return bearerError("invalid_token", translate(locals.locale, "oidc.errors.access_token_invalid")); } // ctrls H-OIDC-2: aud 강제. claims.aud (있을 경우) 가 claims.clientId 와 일치해야 한다. // 또한 발급 클라이언트가 현재도 등록되어 있고 enabled 인지 확인 — 비활성/삭제된 // 클라이언트의 토큰은 더 이상 userinfo 접근 불가. if (claims.aud && claims.aud !== claims.clientId) { - return bearerError("invalid_token", "토큰의 aud 와 client 가 일치하지 않습니다."); + return bearerError("invalid_token", translate(locals.locale, "oidc.errors.token_aud_mismatch")); } const [issuingClient] = await db .select({ id: oidcClients.id }) @@ -52,7 +53,7 @@ async function handleUserinfo(locals: App.Locals, request: Request): Promise { const { locals, platform, url } = event; const { db, tenant } = requireDbContext(locals); - const signingKeySecret = locals.runtimeConfig.signingKeySecret; - if (!signingKeySecret) return; + const signingKeySecrets = locals.runtimeConfig.signingKeySecrets; + if (signingKeySecrets.length === 0) return; const bcTargets = await getOidcBackchannelTargets(db, tenant.id, idpSession.id); if (bcTargets.length === 0) return; - const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecret); + const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecrets); if (!signingKey) return; const issuerUrl = resolveIssuerUrl(locals.runtimeConfig, url.origin); @@ -72,7 +73,7 @@ async function redirectToNextSp(event: RequestEvent, stateId: string, remaining: const { db, tenant } = requireDbContext(locals); if (remaining.length === 0) { - throw error(500, "체인에 남은 SP 가 없습니다"); + throw error(500, translate(locals.locale, "saml.errors.slo_no_remaining_sp")); } const next = remaining[0]; @@ -84,13 +85,13 @@ async function redirectToNextSp(event: RequestEvent, stateId: string, remaining: .set({ pendingSpDataJson: JSON.stringify(rest) }) .where(eq(samlSloStates.id, stateId)); - const signingKeySecret = locals.runtimeConfig.signingKeySecret; - if (!signingKeySecret) { - throw error(500, "서명 키가 설정되지 않아 SLO 체인을 계속 진행할 수 없습니다"); + const signingKeySecrets = locals.runtimeConfig.signingKeySecrets; + if (signingKeySecrets.length === 0) { + throw error(500, translate(locals.locale, "saml.errors.slo_signing_key_not_configured")); } - const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecret); + const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecrets); if (!signingKey) { - throw error(500, "활성 서명 키가 없습니다"); + throw error(500, translate(locals.locale, "saml.errors.slo_active_signing_key_missing")); } const issuerUrl = resolveIssuerUrl(locals.runtimeConfig, url.origin); @@ -169,7 +170,7 @@ export const GET: RequestHandler = async (event) => { .where(and(eq(samlSloStates.id, relayState), eq(samlSloStates.tenantId, tenant.id), gt(samlSloStates.expiresAt, new Date()))) .limit(1); if (!state) { - throw error(400, "Invalid or expired SLO state"); + throw error(400, translate(locals.locale, "saml.errors.slo_state_invalid")); } // ctrls C-9: SP-initiated 흐름의 LogoutResponse 는 반드시 initiatingSp 의 cert @@ -183,15 +184,15 @@ export const GET: RequestHandler = async (event) => { .where(and(eq(samlSps.tenantId, tenant.id), eq(samlSps.entityId, state.initiatingSpEntityId))) .limit(1); if (!initiatingSp?.cert) { - throw error(400, "initiating SP 인증서가 등록되지 않아 LogoutResponse 를 검증할 수 없습니다."); + throw error(400, translate(locals.locale, "saml.errors.slo_initiating_sp_cert_missing")); } if (!url.searchParams.has("Signature")) { - throw error(400, "LogoutResponse 는 반드시 서명되어야 합니다."); + throw error(400, translate(locals.locale, "saml.errors.slo_logout_response_must_be_signed")); } const rawQuery = url.search.replace(/^\?/, ""); const valid = await verifySamlRedirectSignature(rawQuery, initiatingSp.cert); if (!valid) { - throw error(400, "LogoutResponse 서명 검증 실패"); + throw error(400, translate(locals.locale, "saml.errors.slo_logout_response_sig_invalid")); } } @@ -209,9 +210,9 @@ export const GET: RequestHandler = async (event) => { // SP-initiated 였다면 최초 SP 로 LogoutResponse 를 돌려준다. if (state.initiatorSloUrl && state.inResponseTo) { - const signingKeySecret = locals.runtimeConfig.signingKeySecret; - if (signingKeySecret) { - const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecret); + const signingKeySecrets = locals.runtimeConfig.signingKeySecrets; + if (signingKeySecrets.length > 0) { + const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecrets); if (signingKey) { const issuerUrl = resolveIssuerUrl(locals.runtimeConfig, url.origin); const responseXml = buildSamlLogoutResponse({ @@ -244,7 +245,7 @@ export const GET: RequestHandler = async (event) => { .where(and(eq(samlSloStates.id, stateParam), eq(samlSloStates.tenantId, tenant.id), gt(samlSloStates.expiresAt, new Date()))) .limit(1); if (!state) { - throw error(400, "Invalid or expired SLO state"); + throw error(400, translate(locals.locale, "saml.errors.slo_state_invalid")); } const pending = parsePendingSpData(state.pendingSpDataJson); @@ -266,7 +267,7 @@ export const GET: RequestHandler = async (event) => { // 파서가 DOCTYPE/ENTITY 차단 + onErrorStopParsing + IssueInstant skew(±5분) 를 강제한다. parsed = await parseSamlLogoutRequest(samlRequest); } catch { - throw error(400, "Invalid SAMLRequest"); + throw error(400, translate(locals.locale, "saml.errors.slo_invalid_saml_request")); } // Destination 검증: SP 가 명시했으면 IdP 의 SLO endpoint 와 정확히 일치해야 한다. @@ -274,7 +275,7 @@ export const GET: RequestHandler = async (event) => { if (parsed.destination) { const expectedDestination = `${cfgIssuer.replace(/\/+$/, "")}/saml/slo`; if (parsed.destination !== expectedDestination) { - throw error(400, "LogoutRequest Destination 이 IdP 의 SLO endpoint 와 일치하지 않습니다."); + throw error(400, translate(locals.locale, "saml.errors.slo_destination_mismatch")); } } @@ -285,7 +286,7 @@ export const GET: RequestHandler = async (event) => { .where(and(eq(samlSps.tenantId, tenant.id), eq(samlSps.entityId, parsed.issuer), eq(samlSps.enabled, true))) .limit(1); if (!sp) { - throw error(400, "Unknown SAML SP"); + throw error(400, translate(locals.locale, "saml.errors.slo_unknown_sp")); } // ctrls C-8: 모든 SP-initiated LogoutRequest 는 SP cert 로 서명 검증되어야 @@ -293,15 +294,15 @@ export const GET: RequestHandler = async (event) => { // 위조한 LogoutRequest 로 임의 사용자 강제 로그아웃 + SLO 체인을 트리거할 수 // 있다. Signature 파라미터 누락도 동일하게 거부. if (!sp.cert) { - throw error(400, "SP 인증서가 등록되지 않아 SLO 요청을 검증할 수 없습니다."); + throw error(400, translate(locals.locale, "saml.errors.slo_sp_cert_missing")); } if (!url.searchParams.has("Signature")) { - throw error(400, "LogoutRequest 는 반드시 서명되어야 합니다."); + throw error(400, translate(locals.locale, "saml.errors.slo_logout_request_must_be_signed")); } const rawQuery = url.search.replace(/^\?/, ""); const valid = await verifySamlRedirectSignature(rawQuery, sp.cert); if (!valid) { - throw error(400, "Invalid SAMLRequest signature"); + throw error(400, translate(locals.locale, "saml.errors.slo_saml_request_sig_invalid")); } // Replay 가드: 서명 검증 성공 후 LogoutRequest ID 를 1회용으로 소비한다. @@ -315,7 +316,7 @@ export const GET: RequestHandler = async (event) => { .where(and(eq(samlAuthnRequestIds.tenantId, tenant.id), eq(samlAuthnRequestIds.requestId, parsed.id), gt(samlAuthnRequestIds.expiresAt, now))) .limit(1); if (seen) { - throw error(400, "LogoutRequest ID 가 이미 사용되었습니다 (replay)"); + throw error(400, translate(locals.locale, "saml.errors.slo_logout_request_replay")); } try { await db.insert(samlAuthnRequestIds).values({ @@ -326,7 +327,7 @@ export const GET: RequestHandler = async (event) => { }); } catch { // unique 충돌 → replay 와 동일 처리 - throw error(400, "LogoutRequest ID 가 이미 사용되었습니다 (replay)"); + throw error(400, translate(locals.locale, "saml.errors.slo_logout_request_replay")); } } @@ -393,11 +394,11 @@ export const GET: RequestHandler = async (event) => { if (!sp.sloUrl) { throw redirect(302, "/"); } - const signingKeySecret = locals.runtimeConfig.signingKeySecret; - if (!signingKeySecret) { + const signingKeySecrets = locals.runtimeConfig.signingKeySecrets; + if (signingKeySecrets.length === 0) { throw redirect(302, "/"); } - const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecret); + const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecrets); if (!signingKey) { throw redirect(302, "/"); } @@ -422,7 +423,7 @@ export const GET: RequestHandler = async (event) => { // 남은 SP 가 있으면: samlSloState 생성 후 첫 SP 로 체인 시작 if (!idpSession) { // 이론적으로 pending.length > 0 이려면 idpSession 이 있었어야 한다. - throw error(500, "SLO 체인 초기화 실패"); + throw error(500, translate(locals.locale, "saml.errors.slo_chain_init_failed")); } const stateId = crypto.randomUUID(); diff --git a/src/routes/saml/sso/+server.ts b/src/routes/saml/sso/+server.ts index bfe9160..6cc5983 100644 --- a/src/routes/saml/sso/+server.ts +++ b/src/routes/saml/sso/+server.ts @@ -32,6 +32,8 @@ import { buildSignedSamlErrorResponse, buildSignedSamlResponse } from "$lib/serv import { findSp, recordSamlSession, type SamlSpRecord } from "$lib/server/saml/sp"; import { getUserMembership } from "$lib/server/org/membership"; import { getActiveAssignment, parseAssignmentAttributes } from "$lib/server/access/service-permissions"; +import { translate } from "$lib/i18n/server"; +import type { Locale } from "$lib/i18n/core"; const SAML_AUTHN_REQUEST_TTL_MS = 10 * 60 * 1000; // 10분 @@ -40,10 +42,10 @@ function htmlEscape(s: string): string { } /** ACS 로 SAMLResponse 를 실어 보내는 HTTP-POST auto-submit 폼 응답. */ -function renderAutoSubmitForm(acsUrl: string, samlResponseB64: string, relayState: string | null): Response { +function renderAutoSubmitForm(acsUrl: string, samlResponseB64: string, relayState: string | null, locale: Locale): Response { const relayStateInput = relayState ? `` : ""; const html = - `SSO 리다이렉트 중...` + + `${htmlEscape(translate(locale, "saml.errors.sso_redirecting"))}` + `` + `
` + `${relayStateInput}` + @@ -60,6 +62,7 @@ async function buildAndRenderSamlError(params: { certPem: string; privateKey: CryptoKey; relayState: string | null; + locale: Locale; }): Promise { const errorB64 = await buildSignedSamlErrorResponse({ inResponseTo: params.inResponseTo ?? "", @@ -69,7 +72,7 @@ async function buildAndRenderSamlError(params: { certPem: params.certPem, privateKey: params.privateKey, }); - return renderAutoSubmitForm(params.acsUrl, errorB64, params.relayState); + return renderAutoSubmitForm(params.acsUrl, errorB64, params.relayState, params.locale); } interface GateAndIssueParams { @@ -121,7 +124,7 @@ async function gateAndIssueSamlAssertion(event: Parameters[0], p userAgent: meta.userAgent, detail: { error: "access_denied", reason: "no_service_assignment" }, }); - throw error(403, "이 SP에 대한 권한이 없습니다."); + throw error(403, translate(event.locals.locale, "saml.errors.access_denied")); } // Replay 가드 (SP-initiated 한정). Assertion 발급 직전에 동일 AuthnRequest ID 의 @@ -134,7 +137,7 @@ async function gateAndIssueSamlAssertion(event: Parameters[0], p .where(and(eq(samlAuthnRequestIds.tenantId, tenant.id), eq(samlAuthnRequestIds.requestId, p.inResponseTo), gt(samlAuthnRequestIds.expiresAt, now))) .limit(1); if (seen) { - throw error(400, "AuthnRequest ID 가 이미 사용되었습니다 (replay)"); + throw error(400, translate(event.locals.locale, "saml.errors.authn_request_replay")); } try { await db.insert(samlAuthnRequestIds).values({ @@ -145,7 +148,7 @@ async function gateAndIssueSamlAssertion(event: Parameters[0], p }); } catch { // unique constraint 충돌 → replay 와 동일하게 거부 - throw error(400, "AuthnRequest ID 가 이미 사용되었습니다 (replay)"); + throw error(400, translate(event.locals.locale, "saml.errors.authn_request_replay")); } } @@ -265,7 +268,7 @@ async function gateAndIssueSamlAssertion(event: Parameters[0], p detail: { spEntityId: sp.entityId, nameId, initiatedBy: p.inResponseTo ? "sp" : "idp" }, }); - return renderAutoSubmitForm(p.acsUrl, samlResponseB64, p.relayState); + return renderAutoSubmitForm(p.acsUrl, samlResponseB64, p.relayState, event.locals.locale); } interface ProcessAuthnRequestParams { @@ -303,6 +306,7 @@ async function processSpInitiatedAuthnRequest(event: Parameters[ certPem: p.certPem, privateKey: p.privateKey, relayState: authnRequest.relayState, + locale: event.locals.locale, }); } @@ -352,6 +356,7 @@ async function processSpInitiatedAuthnRequest(event: Parameters[ certPem: p.certPem, privateKey: p.privateKey, relayState: authnRequest.relayState, + locale: event.locals.locale, }); } // 첫 시도: 재인증(MFA 포함)을 강제한다. @@ -382,13 +387,13 @@ async function processSpInitiatedAuthnRequest(event: Parameters[ * 로그인된 사용자가 `?sp=` 로 SP 를 지정하면, 대응되는 AuthnRequest 없이 * IdP 가 먼저 Assertion 을 SP 의 등록된 ACS 로 밀어 준다. InResponseTo 없음. */ -async function handleIdpInitiated(event: Parameters[0], ctx: { db: DB; tenant: Tenant; issuerUrl: string; signingKeySecret: string; spEntityId: string }): Promise { +async function handleIdpInitiated(event: Parameters[0], ctx: { db: DB; tenant: Tenant; issuerUrl: string; signingKeySecrets: string[]; spEntityId: string }): Promise { const { locals, url } = event; const { db, tenant } = ctx; const sp = await findSp(db, tenant.id, ctx.spEntityId); if (!sp) { - throw error(403, `등록되지 않은 SP 입니다: ${ctx.spEntityId}`); + throw error(403, translate(locals.locale, "saml.errors.unknown_sp", { entityId: ctx.spEntityId })); } // 미로그인 시 로그인 페이지로 (로그인 후 동일 IdP-initiated URL 로 복귀) @@ -399,9 +404,9 @@ async function handleIdpInitiated(event: Parameters[0], ctx: { d throw redirect(302, loginUrl.toString()); } - const signingKey = await getActiveSigningKey(db, tenant.id, ctx.signingKeySecret); + const signingKey = await getActiveSigningKey(db, tenant.id, ctx.signingKeySecrets); if (!signingKey || !signingKey.certPem) { - throw error(503, "서명 키가 없습니다. 서버를 재시작하여 키를 생성하세요."); + throw error(503, translate(locals.locale, "saml.errors.signing_key_missing")); } const relayState = url.searchParams.get("RelayState"); @@ -433,31 +438,31 @@ async function ssoPreflight(event: Parameters[0]) { const { ipKey } = getRequestMetadata(event); const rl = await checkRateLimit(db, `saml-sso:${ipKey}`, { windowMs: 60 * 1000, limit: 30 }); if (!rl.allowed) { - throw error(429, "요청이 너무 많습니다. 잠시 후 다시 시도해 주세요."); + throw error(429, translate(locals.locale, "saml.errors.rate_limited")); } - if (!config.issuerUrl) throw error(503, "IDP_ISSUER_URL 미설정"); - if (!config.signingKeySecret) throw error(503, "IDP_SIGNING_KEY_SECRET 미설정"); + if (!config.issuerUrl) throw error(503, translate(locals.locale, "saml.errors.issuer_not_set")); + if (config.signingKeySecrets.length === 0) throw error(503, translate(locals.locale, "saml.errors.signing_key_not_set")); - return { db, tenant, issuerUrl: config.issuerUrl, signingKeySecret: config.signingKeySecret }; + return { db, tenant, issuerUrl: config.issuerUrl, signingKeySecrets: config.signingKeySecrets }; } /** AuthnRequest Destination 이 IdP SSO endpoint 와 일치하는지 검증 (명시된 경우만). */ -function assertDestination(authnRequest: ParsedAuthnRequest, issuerUrl: string): void { +function assertDestination(authnRequest: ParsedAuthnRequest, issuerUrl: string, locale: Locale): void { if (authnRequest.destination) { const expectedDestination = `${issuerUrl.replace(/\/+$/, "")}/saml/sso`; if (authnRequest.destination !== expectedDestination) { - throw error(400, "AuthnRequest Destination 이 IdP 의 SSO endpoint 와 일치하지 않습니다."); + throw error(400, translate(locale, "saml.errors.destination_mismatch")); } } } /** AuthnRequest 의 ACS URL 이 등록된 SP ACS 와 일치하는지 검증하고 최종 ACS 를 반환. */ -function resolveAcsUrl(authnRequest: ParsedAuthnRequest, sp: SamlSpRecord): string { +function resolveAcsUrl(authnRequest: ParsedAuthnRequest, sp: SamlSpRecord, locale: Locale): string { // AuthnRequest 에 ACS 가 명시된 경우 반드시 등록된 SP ACS 와 일치해야 한다. // 다른 URL 을 허용하면 공격자가 서명된 Assertion 을 자신의 서버로 가로챌 수 있다. if (authnRequest.acsUrl && authnRequest.acsUrl !== sp.acsUrl) { - throw error(400, "AuthnRequest의 ACS URL이 등록된 SP ACS URL과 일치하지 않습니다."); + throw error(400, translate(locale, "saml.errors.acs_url_mismatch")); } return sp.acsUrl; } @@ -469,7 +474,7 @@ function resolveAcsUrl(authnRequest: ParsedAuthnRequest, sp: SamlSpRecord): stri */ export const GET: RequestHandler = async (event) => { const { url } = event; - const { db, tenant, issuerUrl, signingKeySecret } = await ssoPreflight(event); + const { db, tenant, issuerUrl, signingKeySecrets } = await ssoPreflight(event); const samlRequestB64 = url.searchParams.get("SAMLRequest"); const relayState = url.searchParams.get("RelayState"); @@ -478,9 +483,9 @@ export const GET: RequestHandler = async (event) => { if (!samlRequestB64) { const spParam = url.searchParams.get("sp"); if (spParam) { - return await handleIdpInitiated(event, { db, tenant, issuerUrl, signingKeySecret, spEntityId: spParam }); + return await handleIdpInitiated(event, { db, tenant, issuerUrl, signingKeySecrets, spEntityId: spParam }); } - throw error(400, "SAMLRequest 파라미터가 없습니다."); + throw error(400, translate(event.locals.locale, "saml.errors.saml_request_missing")); } // ── SP-initiated / HTTP-Redirect 바인딩 ──────────────────────────────────── @@ -488,14 +493,14 @@ export const GET: RequestHandler = async (event) => { try { authnRequest = await parseAuthnRequest(samlRequestB64, relayState); } catch { - throw error(400, "SAMLRequest 파싱 실패"); + throw error(400, translate(event.locals.locale, "saml.errors.saml_request_parse_failed")); } - assertDestination(authnRequest, issuerUrl); + assertDestination(authnRequest, issuerUrl, event.locals.locale); const sp = await findSp(db, tenant.id, authnRequest.issuer); if (!sp) { - throw error(403, `등록되지 않은 SP 입니다: ${authnRequest.issuer}`); + throw error(403, translate(event.locals.locale, "saml.errors.unknown_sp", { entityId: authnRequest.issuer })); } // AuthnRequest 서명 검증: SP 가 서명을 요구하거나 Signature 파라미터가 있는 경우. @@ -503,20 +508,20 @@ export const GET: RequestHandler = async (event) => { const hasSig = url.searchParams.has("Signature"); if (sp.wantAuthnRequestsSigned || hasSig) { if (!sp.cert) { - throw error(400, "SP 인증서가 등록되지 않아 AuthnRequest 서명을 검증할 수 없습니다."); + throw error(400, translate(event.locals.locale, "saml.errors.sp_cert_missing_for_authn_sig")); } const rawQuery = url.search.slice(1); const sigValid = await verifySamlRedirectSignature(rawQuery, sp.cert); if (!sigValid) { - throw error(400, "AuthnRequest 서명 검증에 실패했습니다."); + throw error(400, translate(event.locals.locale, "saml.errors.authn_signature_invalid")); } } - const acsUrl = resolveAcsUrl(authnRequest, sp); + const acsUrl = resolveAcsUrl(authnRequest, sp, event.locals.locale); - const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecret); + const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecrets); if (!signingKey || !signingKey.certPem) { - throw error(503, "서명 키가 없습니다. 서버를 재시작하여 키를 생성하세요."); + throw error(503, translate(event.locals.locale, "saml.errors.signing_key_missing")); } return await processSpInitiatedAuthnRequest(event, { @@ -538,14 +543,14 @@ export const GET: RequestHandler = async (event) => { */ export const POST: RequestHandler = async (event) => { const { url } = event; - const { db, tenant, issuerUrl, signingKeySecret } = await ssoPreflight(event); + const { db, tenant, issuerUrl, signingKeySecrets } = await ssoPreflight(event); const form = await event.request.formData(); const samlRequestB64 = typeof form.get("SAMLRequest") === "string" ? (form.get("SAMLRequest") as string) : null; const relayState = typeof form.get("RelayState") === "string" ? (form.get("RelayState") as string) : null; if (!samlRequestB64) { - throw error(400, "SAMLRequest 파라미터가 없습니다."); + throw error(400, translate(event.locals.locale, "saml.errors.saml_request_missing")); } // HTTP-POST 바인딩: base64(XML), deflate 없음. 서명 검증·resume 재인코딩에 재사용하도록 @@ -558,21 +563,21 @@ export const POST: RequestHandler = async (event) => { for (let i = 0; i < raw.length; i++) bin[i] = raw.charCodeAt(i); xml = new TextDecoder().decode(bin); } catch { - throw error(400, "SAMLRequest 파싱 실패"); + throw error(400, translate(event.locals.locale, "saml.errors.saml_request_parse_failed")); } let authnRequest: ParsedAuthnRequest; try { authnRequest = await parseAuthnRequestPost(samlRequestB64, relayState); } catch { - throw error(400, "SAMLRequest 파싱 실패"); + throw error(400, translate(event.locals.locale, "saml.errors.saml_request_parse_failed")); } - assertDestination(authnRequest, issuerUrl); + assertDestination(authnRequest, issuerUrl, event.locals.locale); const sp = await findSp(db, tenant.id, authnRequest.issuer); if (!sp) { - throw error(403, `등록되지 않은 SP 입니다: ${authnRequest.issuer}`); + throw error(403, translate(event.locals.locale, "saml.errors.unknown_sp", { entityId: authnRequest.issuer })); } // ── 서명 검증 (HTTP-POST 바인딩) ─────────────────────────────────────────── @@ -583,19 +588,19 @@ export const POST: RequestHandler = async (event) => { if (sp.wantAuthnRequestsSigned || authnRequest.hasSignature) { if (!sp.cert) { // 검증에 쓸 SP 인증서가 없으면 서명을 검증할 방법이 없다 → 거부. - throw error(400, "SP 인증서가 등록되지 않아 AuthnRequest 서명을 검증할 수 없습니다."); + throw error(400, translate(event.locals.locale, "saml.errors.sp_cert_missing_for_authn_sig")); } const sigValid = await verifyEnvelopedXmlSignature(xml, sp.cert); if (!sigValid) { - throw error(400, "AuthnRequest 서명 검증에 실패했습니다."); + throw error(400, translate(event.locals.locale, "saml.errors.authn_signature_invalid")); } } - const acsUrl = resolveAcsUrl(authnRequest, sp); + const acsUrl = resolveAcsUrl(authnRequest, sp, event.locals.locale); - const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecret); + const signingKey = await getActiveSigningKey(db, tenant.id, signingKeySecrets); if (!signingKey || !signingKey.certPem) { - throw error(503, "서명 키가 없습니다. 서버를 재시작하여 키를 생성하세요."); + throw error(503, translate(event.locals.locale, "saml.errors.signing_key_missing")); } // 로그인/재인증 후 복귀 URL: POST body 는 GET 리다이렉트로 보존되지 않으므로, 동일 diff --git a/test/unit/crypto-keys.test.ts b/test/unit/crypto-keys.test.ts index 3c94000..4385ac0 100644 --- a/test/unit/crypto-keys.test.ts +++ b/test/unit/crypto-keys.test.ts @@ -12,6 +12,8 @@ import { unwrapPrivateKey, encryptSecret, decryptSecret, + tryWithSecrets, + tryWithSecretsNullable, type AccessTokenClaims, } from "$lib/server/crypto/keys"; @@ -258,3 +260,74 @@ describe("secret 암호화 + HKDF 도메인 분리 (encryptSecret / decryptSecre await expect(decryptSecret("only.two", secret)).rejects.toThrow(/Invalid encrypted secret format/); }); }); + +// ── 무중단 회전 헬퍼 (tryWithSecrets / tryWithSecretsNullable) ────────────────── +describe("무중단 시크릿 회전 (tryWithSecrets)", () => { + const CURRENT = "new-master-secret-abcdef"; + const PREVIOUS = "old-master-secret-012345"; + + it("throwing 변형: current 로 복호 성공 시 previous 는 시도하지 않는다", async () => { + // current 로 암호화된 값 → [current, previous] 로 즉시 성공. + const enc = await encryptSecret("payload", CURRENT, "ctx"); + const tried: string[] = []; + const out = await tryWithSecrets([CURRENT, PREVIOUS], (s) => { + tried.push(s); + return decryptSecret(enc, s, "ctx"); + }); + expect(out).toBe("payload"); + expect(tried).toEqual([CURRENT]); // previous 미시도 + }); + + it("throwing 변형: current 실패 시 previous 로 fallback 복호", async () => { + // previous 로 암호화된 (회전 전) 값 → current 실패 후 previous 성공. + const encOld = await encryptSecret("legacy", PREVIOUS, "ctx"); + const tried: string[] = []; + const out = await tryWithSecrets([CURRENT, PREVIOUS], (s) => { + tried.push(s); + return decryptSecret(encOld, s, "ctx"); + }); + expect(out).toBe("legacy"); + expect(tried).toEqual([CURRENT, PREVIOUS]); + }); + + it("throwing 변형: 전부 실패 시 마지막 에러를 throw", async () => { + const encOther = await encryptSecret("x", "totally-different-secret", "ctx"); + await expect(tryWithSecrets([CURRENT, PREVIOUS], (s) => decryptSecret(encOther, s, "ctx"))).rejects.toThrow(); + }); + + it("throwing 변형: 빈 시크릿 배열은 즉시 throw (미설정 방어)", async () => { + await expect(tryWithSecrets([], async () => "x")).rejects.toThrow(); + }); + + it("nullable 변형: 최초 non-null 을 반환하고 이후 시크릿은 시도하지 않는다", async () => { + const claims: AccessTokenClaims = { sub: "u", tenantId: "t", clientId: "c", scope: "openid", iat: 0, exp: Math.floor(Date.now() / 1000) + 300 }; + const token = await generateAccessToken(claims, CURRENT); + const tried: string[] = []; + const out = await tryWithSecretsNullable([CURRENT, PREVIOUS], (s) => { + tried.push(s); + return verifyAccessToken(token, s, "t"); + }); + expect(out?.sub).toBe("u"); + expect(tried).toEqual([CURRENT]); + }); + + it("nullable 변형: current(null) → previous 로 fallback", async () => { + const claims: AccessTokenClaims = { sub: "u2", tenantId: "t", clientId: "c", scope: "openid", iat: 0, exp: Math.floor(Date.now() / 1000) + 300 }; + // previous 로 서명된 (회전 전 발급) 토큰. + const token = await generateAccessToken(claims, PREVIOUS); + const tried: string[] = []; + const out = await tryWithSecretsNullable([CURRENT, PREVIOUS], (s) => { + tried.push(s); + return verifyAccessToken(token, s, "t"); + }); + expect(out?.sub).toBe("u2"); + expect(tried).toEqual([CURRENT, PREVIOUS]); + }); + + it("nullable 변형: 전부 null 이면 null", async () => { + const claims: AccessTokenClaims = { sub: "u3", tenantId: "t", clientId: "c", scope: "openid", iat: 0, exp: Math.floor(Date.now() / 1000) + 300 }; + const token = await generateAccessToken(claims, "some-other-secret"); + const out = await tryWithSecretsNullable([CURRENT, PREVIOUS], (s) => verifyAccessToken(token, s, "t")); + expect(out).toBeNull(); + }); +}); From c1fa64294b796af4c1416ab9b9d1cccc499beb3f Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 13:46:44 +0900 Subject: [PATCH 13/26] =?UTF-8?q?refactor(i18n):=20P10=20=EC=9E=94?= =?UTF-8?q?=EC=A1=B4=20=ED=95=9C=EA=B5=AD=EC=96=B4=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EB=AC=B8=EC=9E=90=EC=97=B4=20=EC=A0=84=EB=9F=89=20i18n=20?= =?UTF-8?q?=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로토콜 라우트 에러 문구 i18n: oidc(authorize/revoke)·saml(metadata)·totp enroll·webauthn(authenticate/register/passkey) + P9 에서 함께 전환한 회전 소비처 라우트의 키 - admin 라우트: ldap-providers·saml-sps(목록/[id]) 서버 에러 i18n - validation·admin/schemas·crud-factory: zod 메시지 i18n 화, admin-zod·crud-factory 테스트 계약 갱신 - i18n ko/en JSON: 프로토콜/admin/기능 키 + mfa_login 5키 대칭 추가(본 후속 라운드 전체가 참조하는 키를 일괄 반영) Co-Authored-By: Claude Fable 5 --- src/lib/i18n/en.json | 174 +++++++++++++++++- src/lib/i18n/ko.json | 174 +++++++++++++++++- src/lib/server/admin/crud-factory.ts | 17 +- src/lib/server/admin/schemas.ts | 31 ++-- src/lib/server/validation.ts | 22 ++- .../admin/ldap-providers/+page.server.ts | 8 +- src/routes/admin/saml-sps/+page.server.ts | 8 +- .../admin/saml-sps/[id]/+page.server.ts | 15 +- src/routes/api/totp/enroll/confirm/+server.ts | 9 +- .../webauthn/authenticate/options/+server.ts | 5 +- .../webauthn/authenticate/verify/+server.ts | 17 +- .../api/webauthn/passkey-client/+server.ts | 21 ++- .../api/webauthn/register/options/+server.ts | 5 +- src/routes/oidc/authorize/+server.ts | 29 +-- src/routes/oidc/revoke/+server.ts | 5 +- src/routes/saml/metadata/+server.ts | 3 +- test/unit/admin-zod.test.ts | 6 +- test/unit/crud-factory.test.ts | 2 +- 18 files changed, 454 insertions(+), 97 deletions(-) diff --git a/src/lib/i18n/en.json b/src/lib/i18n/en.json index 65b3e57..b7b582c 100644 --- a/src/lib/i18n/en.json +++ b/src/lib/i18n/en.json @@ -55,7 +55,13 @@ "err_rate_limit": "Too many login attempts. Please try again in {{minutes}} minutes.", "err_account_locked": "Too many login attempts. Please try again in {{minutes}} minutes.", "err_invalid_credentials": "The username or password is incorrect.", - "err_mfa_config": "There is a problem with the two-factor authentication configuration. Please contact your administrator." + "err_mfa_config": "There is a problem with the two-factor authentication configuration. Please contact your administrator.", + "err_account_deleting": "This account is being deleted and can no longer be used to sign in.", + "deletion_requested_notice": "Your account deletion has been received. You can recover your account by signing in again within the 30-day grace period.", + "recovery_desc": "This account is scheduled for deletion. To keep using it, re-enter your password to recover the account.", + "recovery_password_label": "Re-enter your password to recover", + "recovery_confirm": "Recover account and sign in", + "recovery_cancel": "← Back to login" }, "admin": { "title": "Admin Dashboard", @@ -85,6 +91,7 @@ "required_field_missing": "Required fields are missing.", "signing_key_secret_missing": "IDP_SIGNING_KEY_SECRET is not configured.", "email_password_required": "Email and password are required.", + "email_required": "Email is required.", "invalid_role": "Invalid role.", "email_taken": "This email is already in use.", "username_taken": "This username is already in use.", @@ -156,7 +163,14 @@ "http_localhost_only": "http URLs are only allowed for localhost/127.0.0.1: {{value}}", "custom_scheme_invalid": "Invalid custom scheme format: {{value}}", "scheme_not_allowed": "Scheme is not allowed ({{scheme}}): {{value}}", - "scope_not_allowed": "Scope is not allowed: {{scope}}" + "scope_not_allowed": "Scope is not allowed: {{scope}}", + "saml_url_invalid_format": "{{label}}: The URL format is invalid.", + "saml_url_http_loopback_only": "{{label}}: http URLs are only allowed for localhost/127.0.0.1.", + "saml_url_https_only": "{{label}}: Only https URLs are allowed.", + "ldap_metadata_host_forbidden": "Cloud metadata hosts are not allowed.", + "ldap_linklocal_forbidden": "link-local (169.254/16) addresses are not allowed.", + "ldap_port_invalid": "The port number is invalid.", + "ldap_port_not_allowed": "This LDAP port is not allowed (allowed: {{allowed}})." } }, "admin_login": { @@ -170,7 +184,40 @@ "account": { "profile": "My Profile", "mfa": "Two-Factor Authentication", - "passkeys": "Passkeys" + "passkeys": "Passkeys", + "sessions": { + "title": "Active sessions", + "subtitle": "Review the devices and sessions currently signed in, and sign out any you don't recognize.", + "current_badge": "Current session", + "ip_label": "IP", + "device_label": "Device", + "last_seen_label": "Last active", + "created_label": "Signed in", + "unknown": "Unknown", + "revoke": "Sign out", + "revoke_current": "Sign out this device", + "revoke_confirm": "Sign out this session?", + "revoke_current_confirm": "This is the device you're currently using. Signing out will log you out here too. Continue?", + "revoke_others": "Sign out all other devices", + "revoke_others_confirm": "Sign out every session except this device?", + "empty": "No active sessions.", + "revoked_notice": "The selected session was signed out.", + "revoked_others_notice": "Signed out of all other devices." + }, + "danger_zone": { + "title": "Delete account", + "subtitle": "Move your account into a pending-deletion state. During the grace period you can recover it by signing in again.", + "warning_title": "Requesting deletion will do the following.", + "warning_grace": "If you request deletion now, your account will be permanently deleted in {{days}} days. Until then you can recover it by signing in.", + "warning_recovery": "You will be signed out of all devices immediately, and once the grace period passes your account and related data are permanently deleted.", + "password_label": "Password for identity verification", + "totp_label": "Authenticator code (TOTP)", + "confirm_label": "I understand the above and want to delete my account.", + "confirm_dialog": "Are you sure you want to request deletion? You will be signed out immediately and permanently deleted after the grace period.", + "submit": "Request account deletion", + "err_reauth": "Identity verification failed. Please check your password or authentication code.", + "err_last_admin": "The last active administrator cannot delete their account. Please assign another administrator first." + } }, "logout": { "in_progress": "Logging out..." @@ -184,7 +231,12 @@ "confirm": "Confirm", "use_backup": "Sign in with a backup code", "use_totp": "Sign in with an authenticator code", - "back_to_login": "← Back to login" + "back_to_login": "← Back to login", + "err_config": "There is a problem with your MFA configuration. Please try again later.", + "err_rate_limit": "Too many verification attempts. Please try again later.", + "err_missing_code": "Please enter your code.", + "err_invalid_totp": "Invalid authentication code.", + "err_invalid_backup": "Invalid backup code." }, "mfa_manage": { "title": "Two-Factor Authentication Settings", @@ -335,6 +387,11 @@ "users": { "title": "User Management", "add_btn": "+ Add User", + "invite_btn": "Invite User", + "invite_title": "Invite User", + "invite_hint": "Creates an account without a password and sends an invitation email. The invited user sets their own password from the link.", + "invite_submit": "Send Invite", + "badge_pending_invite": "Pending Invite", "search_placeholder": "Search by email, username, or name", "search_empty": "No results found.", "create_title": "Add New User", @@ -475,7 +532,35 @@ "empty": "No clients have been registered.", "regenerate_secret": "Regenerate Secret", "regenerate_confirm": "Regenerate the secret? The existing secret will be invalidated immediately.", - "delete_confirm": "Delete this client?" + "delete_confirm": "Delete this client?", + "errors": { + "rate_limited": "Too many requests. Please try again shortly.", + "rate_limited_short": "Too many requests.", + "client_id_redirect_uri_required": "client_id and redirect_uri are required.", + "response_type_unsupported": "Only response_type=code is supported.", + "unknown_client": "Unknown client_id.", + "redirect_uri_mismatch": "redirect_uri does not match a registered value.", + "pkce_required": "A PKCE code_challenge is required.", + "empty_code_challenge": "code_challenge is empty.", + "code_challenge_method_unsupported": "Only code_challenge_method=S256 is supported.", + "openid_scope_required": "The openid scope is required.", + "prompt_none_conflict": "prompt=none cannot be combined with other prompt values.", + "max_age_invalid": "max_age must be a non-negative integer.", + "login_required": "Cannot complete authentication without user interaction.", + "session_missing_after_gate": "No session found after the authentication gate.", + "service_access_denied": "You do not have access to this service.", + "token_param_required": "The token parameter is required.", + "id_token_hint_required": "id_token_hint is required.", + "aud_mismatch": "aud mismatch", + "cross_origin_post_blocked": "Cross-origin POST blocked.", + "logging_out": "Logging out...", + "signing_key_not_set": "IDP_SIGNING_KEY_SECRET is not configured.", + "bearer_token_required": "A Bearer token is required.", + "access_token_invalid": "Invalid or expired access token.", + "token_aud_mismatch": "The token's aud does not match the client.", + "issuing_client_invalid": "The issuing client is no longer valid.", + "user_not_found": "User not found." + } }, "saml": { "title": "SAML SP Management", @@ -504,7 +589,38 @@ "empty": "No SAML SPs have been registered.", "delete_confirm": "Delete this SP?", "parse_error_xml": "XML parse error", - "parse_error_generic": "Parse failed" + "parse_error_generic": "Parse failed", + "errors": { + "rate_limited": "Too many requests. Please try again shortly.", + "issuer_not_set": "IDP_ISSUER_URL is not configured.", + "signing_key_not_set": "IDP_SIGNING_KEY_SECRET is not configured.", + "signing_key_missing": "No signing key available. Restart the server to generate one.", + "access_denied": "You do not have access to this SP.", + "authn_request_replay": "This AuthnRequest ID has already been used (replay).", + "unknown_sp": "Unregistered SP: {{entityId}}", + "destination_mismatch": "AuthnRequest Destination does not match the IdP SSO endpoint.", + "acs_url_mismatch": "The AuthnRequest ACS URL does not match the registered SP ACS URL.", + "saml_request_missing": "Missing SAMLRequest parameter.", + "saml_request_parse_failed": "Failed to parse SAMLRequest.", + "sp_cert_missing_for_authn_sig": "Cannot verify the AuthnRequest signature because no SP certificate is registered.", + "authn_signature_invalid": "AuthnRequest signature verification failed.", + "sso_redirecting": "Redirecting for SSO...", + "slo_no_remaining_sp": "No SPs remaining in the chain.", + "slo_signing_key_not_configured": "Cannot continue the SLO chain because no signing key is configured.", + "slo_active_signing_key_missing": "No active signing key available.", + "slo_state_invalid": "Invalid or expired SLO state.", + "slo_initiating_sp_cert_missing": "Cannot verify the LogoutResponse because the initiating SP certificate is not registered.", + "slo_logout_response_must_be_signed": "The LogoutResponse must be signed.", + "slo_logout_response_sig_invalid": "LogoutResponse signature verification failed.", + "slo_invalid_saml_request": "Invalid SAMLRequest.", + "slo_destination_mismatch": "LogoutRequest Destination does not match the IdP SLO endpoint.", + "slo_unknown_sp": "Unknown SAML SP.", + "slo_sp_cert_missing": "Cannot verify the SLO request because no SP certificate is registered.", + "slo_logout_request_must_be_signed": "The LogoutRequest must be signed.", + "slo_saml_request_sig_invalid": "Invalid SAMLRequest signature.", + "slo_logout_request_replay": "This LogoutRequest ID has already been used (replay).", + "slo_chain_init_failed": "Failed to initialize the SLO chain." + } }, "roles": { "list_title": "Roles", @@ -569,6 +685,19 @@ "invalid_link": "This link has expired or is invalid.", "go_login": "Go to login" }, + "accept_invite": { + "title": "Accept Invitation", + "subtitle": "Set a password to complete your registration.", + "password_label": "Password", + "password_placeholder": "At least 8 characters", + "confirm_label": "Confirm Password", + "submit": "Complete Registration", + "success": "Registration complete. You can now sign in.", + "invalid_link": "This link has expired or is invalid.", + "go_login": "Go to login", + "err_password_short": "Password must be at least 8 characters.", + "err_password_mismatch": "Passwords do not match." + }, "errors": { "rate_limit": "Too many requests. Please try again in {{minutes}} minute(s).", "db_not_ready": "The D1 binding \"DB\" is not ready. Please run in a Wrangler preview/dev environment." @@ -620,6 +749,11 @@ "subject": "[Security Alert] A passkey was removed", "heading": "A passkey was removed", "body": "A passkey was removed from your account." + }, + "account_deletion_requested": { + "subject": "[Security Alert] Account deletion requested", + "heading": "Account deletion requested", + "body": "Your account deletion was received and moved to a pending-deletion state. You can recover it by signing in again within the 30-day grace period. If you did not request this, sign in immediately to recover and change your password." } }, "skins": { @@ -689,5 +823,33 @@ "guide_example_login": "Log in", "guide_example_username": "Username", "guide_example_password": "Password" + }, + "webauthn": { + "errors": { + "login_required": "Sign-in required.", + "signing_key_not_configured": "IDP_SIGNING_KEY_SECRET is not configured.", + "invalid_origin": "Invalid origin.", + "register_session_expired": "The registration session has expired. Please try again.", + "register_session_invalid": "The registration session is invalid. Please try again.", + "register_rate_limited": "Too many registration attempts. Please try again shortly.", + "register_verify_failed": "Passkey registration verification failed.", + "rate_limited_retry": "Too many requests. Please try again in {{seconds}} seconds.", + "auth_rate_limited": "Too many authentication attempts. Please try again shortly.", + "auth_session_invalid": "The authentication session is invalid.", + "auth_session_expired": "The authentication session has expired or was already used.", + "auth_failed": "Passkey authentication failed.", + "account_disabled": "This account is disabled.", + "access_denied": "Access denied.", + "auth_cancelled": "Passkey authentication was cancelled." + } + }, + "totp": { + "errors": { + "signing_key_not_set": "IDP_SIGNING_KEY_SECRET is not configured.", + "verify_rate_limited": "Too many TOTP attempts. Please try again shortly.", + "enroll_rate_limited": "Too many TOTP enrollment attempts. Please try again shortly." + }, + "default_authenticator_label": "TOTP authenticator (dispatcher)", + "default_backup_code_label": "Backup code (dispatcher)" } } diff --git a/src/lib/i18n/ko.json b/src/lib/i18n/ko.json index 91c1971..af21c15 100644 --- a/src/lib/i18n/ko.json +++ b/src/lib/i18n/ko.json @@ -55,7 +55,13 @@ "err_rate_limit": "로그인 시도가 너무 많습니다. {{minutes}}분 후 다시 시도해 주세요.", "err_account_locked": "로그인 시도가 너무 많습니다. {{minutes}}분 후 다시 시도해 주세요.", "err_invalid_credentials": "아이디 또는 비밀번호가 올바르지 않습니다.", - "err_mfa_config": "2단계 인증 설정에 문제가 있습니다. 관리자에게 문의해 주세요." + "err_mfa_config": "2단계 인증 설정에 문제가 있습니다. 관리자에게 문의해 주세요.", + "err_account_deleting": "이 계정은 삭제 처리 중이며 더 이상 로그인할 수 없습니다.", + "deletion_requested_notice": "탈퇴가 접수되었습니다. 유예기간(30일) 내에 다시 로그인하면 계정을 복구할 수 있습니다.", + "recovery_desc": "이 계정은 탈퇴(삭제) 예정 상태입니다. 계속 사용하려면 비밀번호를 다시 입력해 계정을 복구하세요.", + "recovery_password_label": "비밀번호를 다시 입력해 복구", + "recovery_confirm": "계정 복구 후 로그인", + "recovery_cancel": "← 로그인으로 돌아가기" }, "admin": { "title": "관리자 대시보드", @@ -85,6 +91,7 @@ "required_field_missing": "필수 항목이 누락되었습니다.", "signing_key_secret_missing": "IDP_SIGNING_KEY_SECRET 이 설정되지 않았습니다.", "email_password_required": "이메일과 비밀번호는 필수입니다.", + "email_required": "이메일은 필수입니다.", "invalid_role": "역할이 올바르지 않습니다.", "email_taken": "이미 사용 중인 이메일입니다.", "username_taken": "이미 사용 중인 아이디입니다.", @@ -156,7 +163,14 @@ "http_localhost_only": "http URL 은 localhost/127.0.0.1 만 허용됩니다: {{value}}", "custom_scheme_invalid": "커스텀 scheme 형식이 올바르지 않습니다: {{value}}", "scheme_not_allowed": "허용되지 않는 scheme 입니다 ({{scheme}}): {{value}}", - "scope_not_allowed": "허용되지 않는 scope 입니다: {{scope}}" + "scope_not_allowed": "허용되지 않는 scope 입니다: {{scope}}", + "saml_url_invalid_format": "{{label}}: URL 형식이 올바르지 않습니다.", + "saml_url_http_loopback_only": "{{label}}: http URL 은 localhost/127.0.0.1 만 허용됩니다.", + "saml_url_https_only": "{{label}}: https URL 만 허용됩니다.", + "ldap_metadata_host_forbidden": "클라우드 메타데이터 호스트는 사용할 수 없습니다.", + "ldap_linklocal_forbidden": "link-local(169.254/16) 주소는 사용할 수 없습니다.", + "ldap_port_invalid": "포트 번호가 올바르지 않습니다.", + "ldap_port_not_allowed": "허용되지 않는 LDAP 포트입니다 (허용: {{allowed}})." } }, "admin_login": { @@ -170,7 +184,40 @@ "account": { "profile": "내 프로필", "mfa": "2단계 인증", - "passkeys": "패스키" + "passkeys": "패스키", + "sessions": { + "title": "활성 세션", + "subtitle": "현재 로그인된 기기와 세션을 확인하고, 낯선 기기를 로그아웃할 수 있습니다.", + "current_badge": "현재 세션", + "ip_label": "IP", + "device_label": "기기", + "last_seen_label": "마지막 활동", + "created_label": "로그인 시각", + "unknown": "알 수 없음", + "revoke": "로그아웃", + "revoke_current": "이 기기 로그아웃", + "revoke_confirm": "이 세션을 로그아웃할까요?", + "revoke_current_confirm": "현재 사용 중인 기기입니다. 로그아웃하면 이 화면에서도 로그아웃됩니다. 계속할까요?", + "revoke_others": "다른 모든 기기에서 로그아웃", + "revoke_others_confirm": "현재 기기를 제외한 모든 세션을 로그아웃할까요?", + "empty": "활성 세션이 없습니다.", + "revoked_notice": "선택한 세션을 로그아웃했습니다.", + "revoked_others_notice": "다른 모든 기기에서 로그아웃했습니다." + }, + "danger_zone": { + "title": "계정 탈퇴", + "subtitle": "계정을 삭제 예정 상태로 전환합니다. 유예기간 동안은 다시 로그인해 복구할 수 있습니다.", + "warning_title": "탈퇴하면 다음이 진행됩니다.", + "warning_grace": "지금 탈퇴를 신청하면 계정이 {{days}}일 후 영구 삭제됩니다. 그때까지는 로그인해서 복구할 수 있습니다.", + "warning_recovery": "탈퇴 즉시 모든 기기에서 로그아웃되며, 유예기간이 지나면 계정과 관련 데이터가 영구 삭제됩니다.", + "password_label": "본인 확인을 위한 비밀번호", + "totp_label": "인증 앱 코드 (TOTP)", + "confirm_label": "위 내용을 이해했으며 계정 탈퇴를 신청합니다.", + "confirm_dialog": "정말 탈퇴를 신청하시겠습니까? 즉시 로그아웃되며 유예기간이 지나면 영구 삭제됩니다.", + "submit": "계정 탈퇴 신청", + "err_reauth": "본인 확인에 실패했습니다. 비밀번호 또는 인증 코드를 확인해 주세요.", + "err_last_admin": "마지막 활성 관리자는 탈퇴할 수 없습니다. 먼저 다른 관리자를 지정해 주세요." + } }, "logout": { "in_progress": "로그아웃 중..." @@ -184,7 +231,12 @@ "confirm": "확인", "use_backup": "백업 코드로 로그인", "use_totp": "인증 앱 코드로 로그인", - "back_to_login": "← 로그인으로 돌아가기" + "back_to_login": "← 로그인으로 돌아가기", + "err_config": "MFA 설정에 문제가 있습니다. 잠시 후 다시 시도해 주세요.", + "err_rate_limit": "인증 시도가 너무 많습니다. 잠시 후 다시 시도해 주세요.", + "err_missing_code": "코드를 입력해 주세요.", + "err_invalid_totp": "인증 코드가 올바르지 않습니다.", + "err_invalid_backup": "백업 코드가 올바르지 않습니다." }, "mfa_manage": { "title": "2단계 인증 관리", @@ -335,6 +387,11 @@ "users": { "title": "사용자 관리", "add_btn": "+ 사용자 추가", + "invite_btn": "사용자 초대", + "invite_title": "사용자 초대", + "invite_hint": "비밀번호 없이 계정을 만들고 초대 메일을 발송합니다. 초대받은 사용자가 링크에서 비밀번호를 직접 설정합니다.", + "invite_submit": "초대 보내기", + "badge_pending_invite": "초대중", "search_placeholder": "이메일 · 아이디 · 이름 검색", "search_empty": "검색 결과가 없습니다.", "create_title": "새 사용자 추가", @@ -475,7 +532,35 @@ "empty": "등록된 클라이언트가 없습니다.", "regenerate_secret": "시크릿 재생성", "regenerate_confirm": "시크릿을 재생성하시겠습니까? 기존 시크릿은 즉시 무효화됩니다.", - "delete_confirm": "클라이언트를 삭제하시겠습니까?" + "delete_confirm": "클라이언트를 삭제하시겠습니까?", + "errors": { + "rate_limited": "요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.", + "rate_limited_short": "요청이 너무 많습니다.", + "client_id_redirect_uri_required": "client_id 와 redirect_uri 는 필수입니다.", + "response_type_unsupported": "response_type=code 만 지원합니다.", + "unknown_client": "등록되지 않은 client_id 입니다.", + "redirect_uri_mismatch": "redirect_uri 가 등록된 값과 일치하지 않습니다.", + "pkce_required": "PKCE code_challenge 가 필요합니다.", + "empty_code_challenge": "code_challenge 가 비어 있습니다.", + "code_challenge_method_unsupported": "code_challenge_method=S256 만 지원합니다.", + "openid_scope_required": "openid scope 가 필요합니다.", + "prompt_none_conflict": "prompt=none 은 다른 prompt 값과 함께 쓸 수 없습니다.", + "max_age_invalid": "max_age 는 음이 아닌 정수여야 합니다.", + "login_required": "사용자 상호작용 없이 인증을 완료할 수 없습니다.", + "session_missing_after_gate": "인증 게이트 이후 세션이 없습니다.", + "service_access_denied": "이 서비스에 대한 권한이 없습니다.", + "token_param_required": "token 파라미터가 필요합니다.", + "id_token_hint_required": "id_token_hint 가 필요합니다.", + "aud_mismatch": "aud 가 일치하지 않습니다.", + "cross_origin_post_blocked": "cross-origin POST 가 차단되었습니다.", + "logging_out": "로그아웃 중...", + "signing_key_not_set": "IDP_SIGNING_KEY_SECRET 미설정", + "bearer_token_required": "Bearer 토큰이 필요합니다.", + "access_token_invalid": "유효하지 않거나 만료된 액세스 토큰입니다.", + "token_aud_mismatch": "토큰의 aud 와 client 가 일치하지 않습니다.", + "issuing_client_invalid": "발급 client 가 더 이상 유효하지 않습니다.", + "user_not_found": "사용자를 찾을 수 없습니다." + } }, "saml": { "title": "SAML SP 관리", @@ -504,7 +589,38 @@ "empty": "등록된 SAML SP 가 없습니다.", "delete_confirm": "SP를 삭제하시겠습니까?", "parse_error_xml": "XML 파싱 오류", - "parse_error_generic": "파싱 실패" + "parse_error_generic": "파싱 실패", + "errors": { + "rate_limited": "요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.", + "issuer_not_set": "IDP_ISSUER_URL 미설정", + "signing_key_not_set": "IDP_SIGNING_KEY_SECRET 미설정", + "signing_key_missing": "서명 키가 없습니다. 서버를 재시작하여 키를 생성하세요.", + "access_denied": "이 SP에 대한 권한이 없습니다.", + "authn_request_replay": "AuthnRequest ID 가 이미 사용되었습니다 (replay)", + "unknown_sp": "등록되지 않은 SP 입니다: {{entityId}}", + "destination_mismatch": "AuthnRequest Destination 이 IdP 의 SSO endpoint 와 일치하지 않습니다.", + "acs_url_mismatch": "AuthnRequest 의 ACS URL 이 등록된 SP ACS URL 과 일치하지 않습니다.", + "saml_request_missing": "SAMLRequest 파라미터가 없습니다.", + "saml_request_parse_failed": "SAMLRequest 파싱에 실패했습니다.", + "sp_cert_missing_for_authn_sig": "SP 인증서가 등록되지 않아 AuthnRequest 서명을 검증할 수 없습니다.", + "authn_signature_invalid": "AuthnRequest 서명 검증에 실패했습니다.", + "sso_redirecting": "SSO 리다이렉트 중...", + "slo_no_remaining_sp": "체인에 남은 SP 가 없습니다.", + "slo_signing_key_not_configured": "서명 키가 설정되지 않아 SLO 체인을 계속 진행할 수 없습니다.", + "slo_active_signing_key_missing": "활성 서명 키가 없습니다.", + "slo_state_invalid": "SLO 상태가 유효하지 않거나 만료되었습니다.", + "slo_initiating_sp_cert_missing": "initiating SP 인증서가 등록되지 않아 LogoutResponse 를 검증할 수 없습니다.", + "slo_logout_response_must_be_signed": "LogoutResponse 는 반드시 서명되어야 합니다.", + "slo_logout_response_sig_invalid": "LogoutResponse 서명 검증에 실패했습니다.", + "slo_invalid_saml_request": "유효하지 않은 SAMLRequest 입니다.", + "slo_destination_mismatch": "LogoutRequest Destination 이 IdP 의 SLO endpoint 와 일치하지 않습니다.", + "slo_unknown_sp": "알 수 없는 SAML SP 입니다.", + "slo_sp_cert_missing": "SP 인증서가 등록되지 않아 SLO 요청을 검증할 수 없습니다.", + "slo_logout_request_must_be_signed": "LogoutRequest 는 반드시 서명되어야 합니다.", + "slo_saml_request_sig_invalid": "SAMLRequest 서명이 유효하지 않습니다.", + "slo_logout_request_replay": "LogoutRequest ID 가 이미 사용되었습니다 (replay)", + "slo_chain_init_failed": "SLO 체인 초기화에 실패했습니다." + } }, "roles": { "list_title": "Role 목록", @@ -569,6 +685,19 @@ "invalid_link": "링크가 만료되었거나 유효하지 않습니다.", "go_login": "로그인하러 가기" }, + "accept_invite": { + "title": "초대 수락", + "subtitle": "비밀번호를 설정하여 가입을 완료해 주세요.", + "password_label": "비밀번호", + "password_placeholder": "8자 이상", + "confirm_label": "비밀번호 확인", + "submit": "가입 완료", + "success": "가입이 완료되었습니다. 이제 로그인할 수 있습니다.", + "invalid_link": "링크가 만료되었거나 유효하지 않습니다.", + "go_login": "로그인하러 가기", + "err_password_short": "비밀번호는 8자 이상이어야 합니다.", + "err_password_mismatch": "비밀번호가 일치하지 않습니다." + }, "errors": { "rate_limit": "요청이 너무 많습니다. {{minutes}}분 후 다시 시도해 주세요.", "db_not_ready": "D1 binding \"DB\" 가 준비되지 않았습니다. Wrangler preview/dev 환경에서 실행해 주세요." @@ -620,6 +749,11 @@ "subject": "[보안 알림] 패스키가 삭제되었습니다", "heading": "패스키가 삭제되었습니다", "body": "계정에서 패스키가 삭제되었습니다." + }, + "account_deletion_requested": { + "subject": "[보안 알림] 계정 탈퇴가 접수되었습니다", + "heading": "계정 탈퇴가 접수되었습니다", + "body": "계정 탈퇴가 접수되어 삭제 예정 상태로 전환되었습니다. 유예기간(30일) 내에 다시 로그인하면 계정을 복구할 수 있습니다. 본인이 요청하지 않았다면 즉시 로그인해 복구하고 비밀번호를 변경해 주세요." } }, "skins": { @@ -689,5 +823,33 @@ "guide_example_login": "로그인", "guide_example_username": "아이디", "guide_example_password": "비밀번호" + }, + "webauthn": { + "errors": { + "login_required": "로그인이 필요합니다.", + "signing_key_not_configured": "IDP_SIGNING_KEY_SECRET 이 설정되지 않았습니다.", + "invalid_origin": "유효하지 않은 출처입니다.", + "register_session_expired": "등록 세션이 만료되었습니다. 다시 시도해 주세요.", + "register_session_invalid": "등록 세션이 유효하지 않습니다. 다시 시도해 주세요.", + "register_rate_limited": "등록 시도가 너무 많습니다. 잠시 후 다시 시도해 주세요.", + "register_verify_failed": "패스키 등록 검증에 실패했습니다.", + "rate_limited_retry": "요청이 너무 많습니다. {{seconds}}초 후 다시 시도해 주세요.", + "auth_rate_limited": "인증 시도가 너무 많습니다. 잠시 후 다시 시도해 주세요.", + "auth_session_invalid": "인증 세션이 유효하지 않습니다.", + "auth_session_expired": "인증 세션이 만료되었거나 이미 사용되었습니다.", + "auth_failed": "패스키 인증에 실패했습니다.", + "account_disabled": "비활성화된 계정입니다.", + "access_denied": "접근 권한이 없습니다.", + "auth_cancelled": "패스키 인증이 취소되었습니다." + } + }, + "totp": { + "errors": { + "signing_key_not_set": "IDP_SIGNING_KEY_SECRET 미설정", + "verify_rate_limited": "TOTP 시도가 너무 많습니다. 잠시 후 다시 시도해 주세요.", + "enroll_rate_limited": "TOTP 등록 시도가 너무 많습니다. 잠시 후 다시 시도해 주세요." + }, + "default_authenticator_label": "TOTP 인증기 (dispatcher)", + "default_backup_code_label": "백업 코드 (dispatcher)" } } diff --git a/src/lib/server/admin/crud-factory.ts b/src/lib/server/admin/crud-factory.ts index 0a5b744..cb482d3 100644 --- a/src/lib/server/admin/crud-factory.ts +++ b/src/lib/server/admin/crud-factory.ts @@ -18,6 +18,7 @@ import type { Column } from "drizzle-orm"; import type { z } from "zod"; import { requireAdminContext } from "$lib/server/auth/guards"; import { recordAuditEvent, getRequestMetadata } from "$lib/server/audit/index"; +import { translate } from "$lib/i18n/server"; import type { DB } from "$lib/server/db"; type AdminContext = ReturnType; @@ -74,9 +75,13 @@ function formDataToRecord(fd: FormData): Record { return obj; } -/** zod 검증 실패 시 첫 이슈 메시지. */ -function firstIssue(error: z.ZodError): string { - return error.issues[0]?.message ?? "잘못된 요청입니다."; +/** + * zod 검증 실패 시 첫 이슈 메시지를 현재 로케일로 해석한다. + * 스키마 메시지는 i18n 키(admin.errors.*)로 담겨 있으므로 translate 로 표시 문자열을 만든다. + * 키가 아닌 zod 기본 메시지(enum 등)는 translate 폴백이 원문을 그대로 반환한다. + */ +function firstIssue(error: z.ZodError, locale: App.Locals["locale"]): string { + return translate(locale, error.issues[0]?.message ?? "admin.errors.invalid_request"); } /** @@ -96,7 +101,7 @@ export function createAdminCrudRoute; @@ -126,7 +131,7 @@ export function createAdminCrudRoute; @@ -159,7 +164,7 @@ export function createAdminCrudRoute z.preprocess((v) => (typeof v === " /** displayOrder: 유효하지 않거나 빈값이면 0 (기존 parseInt isNaN→0 동작 보존). */ export const displayOrderField = z.coerce.number().int().catch(0); +// 폼 검증 메시지는 i18n 키(전체 경로)로 담는다. 실제 표시 시점(crud-factory)에서 +// translate(locale, key) 로 해석된다 — 스키마는 로케일 비의존 정적 모듈이므로 키만 보관. + // ── teams ───────────────────────────────────────────────────────────────── export const teamCreateSchema = z.object({ - name: requiredText("팀명을 입력해 주세요."), + name: requiredText("admin.errors.team_name_required"), code: optionalText, departmentId: optionalText, description: optionalText, }); export const teamUpdateSchema = z.object({ - id: requiredText("잘못된 요청입니다."), - name: requiredText("잘못된 요청입니다."), + id: requiredText("admin.errors.invalid_request"), + name: requiredText("admin.errors.invalid_request"), code: optionalText, departmentId: optionalText, description: optionalText, @@ -54,14 +57,14 @@ export const teamUpdateSchema = z.object({ // ── parts ───────────────────────────────────────────────────────────────── export const partCreateSchema = z.object({ - name: requiredText("파트명을 입력해 주세요."), + name: requiredText("admin.errors.part_name_required"), code: optionalText, teamId: optionalText, description: optionalText, }); export const partUpdateSchema = z.object({ - id: requiredText("잘못된 요청입니다."), - name: requiredText("잘못된 요청입니다."), + id: requiredText("admin.errors.invalid_request"), + name: requiredText("admin.errors.invalid_request"), code: optionalText, teamId: optionalText, description: optionalText, @@ -70,28 +73,28 @@ export const partUpdateSchema = z.object({ // ── positions ─────────────────────────────────────────────────────────────── export const positionCreateSchema = z.object({ - name: requiredText("직급명을 입력해 주세요."), + name: requiredText("admin.errors.position_name_required"), code: optionalText, - level: intField("레벨은 숫자여야 합니다."), + level: intField("admin.errors.level_must_be_number"), }); export const positionUpdateSchema = z.object({ - id: requiredText("잘못된 요청입니다."), - name: requiredText("잘못된 요청입니다."), + id: requiredText("admin.errors.invalid_request"), + name: requiredText("admin.errors.invalid_request"), code: optionalText, - level: intField("레벨은 숫자여야 합니다."), + level: intField("admin.errors.level_must_be_number"), }); // ── departments ───────────────────────────────────────────────────────────── export const departmentCreateSchema = z.object({ - name: requiredText("부서명을 입력해 주세요."), + name: requiredText("admin.errors.department_name_required"), code: optionalText, parentId: optionalText, description: optionalText, displayOrder: displayOrderField, }); export const departmentUpdateSchema = z.object({ - id: requiredText("잘못된 요청입니다."), - name: requiredText("잘못된 요청입니다."), + id: requiredText("admin.errors.invalid_request"), + name: requiredText("admin.errors.invalid_request"), code: optionalText, parentId: optionalText, description: optionalText, diff --git a/src/lib/server/validation.ts b/src/lib/server/validation.ts index 49823ea..3700771 100644 --- a/src/lib/server/validation.ts +++ b/src/lib/server/validation.ts @@ -4,8 +4,14 @@ * 공통 판정을 한곳으로 모은다. */ +/** + * 검증 실패 사유. 로케일 비의존 — i18n 키(admin.errors.)와 치환 파라미터만 담는다. + * 호출부에서 `adminError(locale, reason.key, reason.params)` 로 표시 문자열을 만든다. + */ +export type ValidationReason = { key: string; params?: Record }; + /** URL/host 검증 결과 공통 형태. */ -export type ValidationResult = { ok: true } | { ok: false; reason: string }; +export type ValidationResult = { ok: true } | { ok: false; reason: ValidationReason }; /** * loopback 호스트 판정. http URL 허용(개발/내부) 여부나 SSRF 게이트에서 공통 사용. @@ -39,15 +45,15 @@ export function validateSamlUrl(value: string, label: string): ValidationResult try { parsed = new URL(value); } catch { - return { ok: false, reason: `${label}: URL 형식이 올바르지 않습니다.` }; + return { ok: false, reason: { key: "saml_url_invalid_format", params: { label } } }; } const scheme = parsed.protocol.replace(/:$/, "").toLowerCase(); if (scheme === "https") return { ok: true }; if (scheme === "http") { if (isLoopbackHost(parsed.hostname)) return { ok: true }; - return { ok: false, reason: `${label}: http URL 은 localhost/127.0.0.1 만 허용됩니다.` }; + return { ok: false, reason: { key: "saml_url_http_loopback_only", params: { label } } }; } - return { ok: false, reason: `${label}: https URL 만 허용됩니다.` }; + return { ok: false, reason: { key: "saml_url_https_only", params: { label } } }; } /** @@ -57,11 +63,11 @@ export function validateSamlUrl(value: string, label: string): ValidationResult export function validateLdapHost(host: string): ValidationResult { const lower = host.toLowerCase(); if (isCloudMetadataHost(lower)) { - return { ok: false, reason: "클라우드 메타데이터 호스트는 사용할 수 없습니다." }; + return { ok: false, reason: { key: "ldap_metadata_host_forbidden" } }; } // 169.254.0.0/16 link-local (AWS IMDS 169.254.169.254 포함) if (isLinkLocalHost(lower)) { - return { ok: false, reason: "link-local(169.254/16) 주소는 사용할 수 없습니다." }; + return { ok: false, reason: { key: "ldap_linklocal_forbidden" } }; } return { ok: true }; } @@ -72,10 +78,10 @@ const ALLOWED_LDAP_PORTS = new Set([389, 636, 3268, 3269]); /** LDAP 포트 검증(정수 범위 + 허용 목록). */ export function validateLdapPort(port: number): ValidationResult { if (!Number.isInteger(port) || port < 1 || port > 65535) { - return { ok: false, reason: "포트 번호가 올바르지 않습니다." }; + return { ok: false, reason: { key: "ldap_port_invalid" } }; } if (!ALLOWED_LDAP_PORTS.has(port)) { - return { ok: false, reason: `허용되지 않는 LDAP 포트입니다 (허용: ${[...ALLOWED_LDAP_PORTS].join(", ")}).` }; + return { ok: false, reason: { key: "ldap_port_not_allowed", params: { allowed: [...ALLOWED_LDAP_PORTS].join(", ") } } }; } return { ok: true }; } diff --git a/src/routes/admin/ldap-providers/+page.server.ts b/src/routes/admin/ldap-providers/+page.server.ts index d3f9fff..ea78a11 100644 --- a/src/routes/admin/ldap-providers/+page.server.ts +++ b/src/routes/admin/ldap-providers/+page.server.ts @@ -102,11 +102,11 @@ export const actions: Actions = { }); const hostV = validateLdapHost(host); - if (!hostV.ok) return fail(400, { create: true, error: hostV.reason }); + if (!hostV.ok) return fail(400, { create: true, error: adminError(locale, hostV.reason.key, hostV.reason.params) }); const port = parseInt(String(fd.get("port") ?? "389"), 10); const portV = validateLdapPort(isNaN(port) ? 389 : port); - if (!portV.ok) return fail(400, { create: true, error: portV.reason }); + if (!portV.ok) return fail(400, { create: true, error: adminError(locale, portV.reason.key, portV.reason.params) }); const { signingKeySecret } = getRuntimeConfig(event.platform); let config: LdapProviderConfig; @@ -152,11 +152,11 @@ export const actions: Actions = { const host = String(fd.get("host") ?? "").trim(); if (host) { const hostV = validateLdapHost(host); - if (!hostV.ok) return fail(400, { error: hostV.reason }); + if (!hostV.ok) return fail(400, { error: adminError(locale, hostV.reason.key, hostV.reason.params) }); } const port = parseInt(String(fd.get("port") ?? "389"), 10); const portV = validateLdapPort(isNaN(port) ? 389 : port); - if (!portV.ok) return fail(400, { error: portV.reason }); + if (!portV.ok) return fail(400, { error: adminError(locale, portV.reason.key, portV.reason.params) }); const { signingKeySecret } = getRuntimeConfig(event.platform); let config: LdapProviderConfig; diff --git a/src/routes/admin/saml-sps/+page.server.ts b/src/routes/admin/saml-sps/+page.server.ts index cd30d6b..a15f087 100644 --- a/src/routes/admin/saml-sps/+page.server.ts +++ b/src/routes/admin/saml-sps/+page.server.ts @@ -84,9 +84,9 @@ export const actions: Actions = { if (encryptAssertion && !cert) return fail(400, { create: true, error: adminError(locale, "sp_cert_required_for_encryption") }); const acsV = validateSamlUrl(acsUrl, "ACS URL"); - if (!acsV.ok) return fail(400, { create: true, error: acsV.reason }); + if (!acsV.ok) return fail(400, { create: true, error: adminError(locale, acsV.reason.key, acsV.reason.params) }); const sloV = validateSamlUrl(sloUrl, "SLO URL"); - if (!sloV.ok) return fail(400, { create: true, error: sloV.reason }); + if (!sloV.ok) return fail(400, { create: true, error: adminError(locale, sloV.reason.key, sloV.reason.params) }); if (!(ALLOWED_NAMEID_FORMATS as readonly string[]).includes(nameIdFormat)) { return fail(400, { create: true, error: adminError(locale, "nameid_format_forbidden") }); @@ -158,9 +158,9 @@ export const actions: Actions = { if (encryptAssertion && !cert) return fail(400, { error: adminError(locale, "sp_cert_required_for_encryption") }); const acsV = validateSamlUrl(acsUrl, "ACS URL"); - if (!acsV.ok) return fail(400, { error: acsV.reason }); + if (!acsV.ok) return fail(400, { error: adminError(locale, acsV.reason.key, acsV.reason.params) }); const sloV = validateSamlUrl(sloUrl, "SLO URL"); - if (!sloV.ok) return fail(400, { error: sloV.reason }); + if (!sloV.ok) return fail(400, { error: adminError(locale, sloV.reason.key, sloV.reason.params) }); if (nameIdFormat && !(ALLOWED_NAMEID_FORMATS as readonly string[]).includes(nameIdFormat)) { return fail(400, { error: adminError(locale, "nameid_format_forbidden") }); diff --git a/src/routes/admin/saml-sps/[id]/+page.server.ts b/src/routes/admin/saml-sps/[id]/+page.server.ts index fd5efd4..eb61340 100644 --- a/src/routes/admin/saml-sps/[id]/+page.server.ts +++ b/src/routes/admin/saml-sps/[id]/+page.server.ts @@ -2,6 +2,7 @@ import { error, fail } from "@sveltejs/kit"; import { and, asc, eq } from "drizzle-orm"; import type { Actions, PageServerLoad } from "./$types"; import { requireAdminContext } from "$lib/server/auth/guards"; +import { adminError } from "$lib/server/admin/errors"; import { recordAuditEvent, getRequestMetadata } from "$lib/server/audit/index"; import type { DB } from "$lib/server/db"; import { samlSps, serviceRoles } from "$lib/server/db/schema"; @@ -16,7 +17,7 @@ export const load: PageServerLoad = async ({ locals, params }) => { .from(samlSps) .where(and(eq(samlSps.id, params.id), eq(samlSps.tenantId, tenant.id))) .limit(1); - if (!sp) error(404, "SP 를 찾을 수 없습니다."); + if (!sp) error(404, adminError(locals.locale, "saml_sp_not_found")); const roles = await db .select() @@ -48,11 +49,11 @@ export const actions: Actions = { const isDefault = fd.get("isDefault") === "true"; const displayOrder = Number(fd.get("displayOrder") ?? "0") | 0; - if (!ROLE_KEY_RE.test(key)) return fail(400, { error: "key 는 영숫자/._- 만 허용 (1~64자)." }); - if (!label) return fail(400, { error: "label 은 필수입니다." }); + if (!ROLE_KEY_RE.test(key)) return fail(400, { error: adminError(locals.locale, "invalid_role_key") }); + if (!label) return fail(400, { error: adminError(locals.locale, "label_required") }); const s = await spForTenant(db, tenant.id, params.id); - if (!s) return fail(404, { error: "SP 를 찾을 수 없습니다." }); + if (!s) return fail(404, { error: adminError(locals.locale, "saml_sp_not_found") }); try { await db.insert(serviceRoles).values({ @@ -67,7 +68,7 @@ export const actions: Actions = { displayOrder, }); } catch { - return fail(409, { error: "이미 존재하는 role key 입니다." }); + return fail(409, { error: adminError(locals.locale, "role_key_exists") }); } const meta = getRequestMetadata(event); @@ -96,7 +97,7 @@ export const actions: Actions = { const isDefault = fd.get("isDefault") === "true"; const displayOrder = Number(fd.get("displayOrder") ?? "0") | 0; - if (!id || !label) return fail(400, { error: "필수 항목 누락." }); + if (!id || !label) return fail(400, { error: adminError(locals.locale, "required_field_missing") }); await db .update(serviceRoles) @@ -111,7 +112,7 @@ export const actions: Actions = { const { db, tenant } = requireAdminContext(locals); const fd = await event.request.formData(); const id = String(fd.get("roleId") ?? ""); - if (!id) return fail(400, { error: "잘못된 요청." }); + if (!id) return fail(400, { error: adminError(locals.locale, "invalid_request") }); await db.delete(serviceRoles).where(and(eq(serviceRoles.id, id), eq(serviceRoles.tenantId, tenant.id), eq(serviceRoles.serviceType, "saml"), eq(serviceRoles.serviceRefId, params.id))); diff --git a/src/routes/api/totp/enroll/confirm/+server.ts b/src/routes/api/totp/enroll/confirm/+server.ts index 868bc16..dfedcb1 100644 --- a/src/routes/api/totp/enroll/confirm/+server.ts +++ b/src/routes/api/totp/enroll/confirm/+server.ts @@ -9,6 +9,7 @@ import { credentials, users } from "$lib/server/db/schema"; import type { DB } from "$lib/server/db"; import { runAtomic } from "$lib/server/db/atomic"; import { isUniqueViolation } from "$lib/server/db/errors"; +import { translate } from "$lib/i18n/server"; /** * Phase 7.3 — TOTP enrollment confirm. @@ -21,14 +22,14 @@ export const POST: RequestHandler = async ({ request, locals }) => { const config = locals.runtimeConfig; if (!config.signingKeySecret) { - throw error(503, "IDP_SIGNING_KEY_SECRET 미설정"); + throw error(503, translate(locals.locale, "totp.errors.signing_key_not_set")); } const body = (await request.json().catch(() => null)) as { userId?: string; secret?: string; code?: string; label?: string } | null; const userId = body?.userId?.trim(); const secret = body?.secret?.trim(); const code = body?.code?.replace(/\s/g, ""); - const label = body?.label?.trim() || "TOTP 인증기 (dispatcher)"; + const label = body?.label?.trim() || translate(locals.locale, "totp.default_authenticator_label"); if (!userId || !secret || !code) { throw error(400, "userId, secret, code required"); @@ -37,7 +38,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { // ctrls C3: enrollment 코드 브루트포스 방어 (사용자당 5분 창 10회). const rl = await checkRateLimit(db, `totp-enroll-confirm:${userId}`, { windowMs: 5 * 60 * 1000, limit: 10 }); if (!rl.allowed) { - throw error(429, "TOTP 등록 시도가 너무 많습니다. 잠시 후 다시 시도해 주세요."); + throw error(429, translate(locals.locale, "totp.errors.enroll_rate_limited")); } const [u] = await db.select({ id: users.id }).from(users).where(eq(users.id, userId)).limit(1); @@ -66,7 +67,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { userId, type: BACKUP_CODE_CREDENTIAL_TYPE, secret: await hashBackupCode(c), - label: "백업 코드 (dispatcher)", + label: translate(locals.locale, "totp.default_backup_code_label"), })), ); diff --git a/src/routes/api/webauthn/authenticate/options/+server.ts b/src/routes/api/webauthn/authenticate/options/+server.ts index d82f6d8..ff68dcc 100644 --- a/src/routes/api/webauthn/authenticate/options/+server.ts +++ b/src/routes/api/webauthn/authenticate/options/+server.ts @@ -4,6 +4,7 @@ import { requireDbContext } from "$lib/server/auth/guards"; import { buildAuthenticationOptions, getWebAuthnConfig, saveChallenge } from "$lib/server/auth/webauthn"; import { checkRateLimit } from "$lib/server/ratelimit"; import { getRequestMetadata } from "$lib/server/audit"; +import { translate } from "$lib/i18n/server"; export const POST: RequestHandler = async (event) => { const { url, locals, request } = event; @@ -13,7 +14,7 @@ export const POST: RequestHandler = async (event) => { // Origin 검증 — 외부 사이트가 challenge 를 강탈/누적시키지 못하게. const reqOrigin = request.headers.get("origin"); if (reqOrigin && reqOrigin !== origin) { - throw error(403, "유효하지 않은 출처입니다."); + throw error(403, translate(locals.locale, "webauthn.errors.invalid_origin")); } // ctrls H-AUTH-2: rate-limit. 익명 호출자가 무제한으로 challenge 를 생성해 @@ -22,7 +23,7 @@ export const POST: RequestHandler = async (event) => { const meta = getRequestMetadata(event); const rl = await checkRateLimit(db, `webauthn-options:${tenant.id}:${meta.ipKey}`, { windowMs: 5 * 60 * 1000, limit: 30 }); if (!rl.allowed) { - throw error(429, `요청이 너무 많습니다. ${Math.ceil(rl.retryAfterMs / 1000)}초 후 다시 시도해 주세요.`); + throw error(429, translate(locals.locale, "webauthn.errors.rate_limited_retry", { seconds: Math.ceil(rl.retryAfterMs / 1000) })); } const options = await buildAuthenticationOptions(rpID); diff --git a/src/routes/api/webauthn/authenticate/verify/+server.ts b/src/routes/api/webauthn/authenticate/verify/+server.ts index 2d36472..4f1b820 100644 --- a/src/routes/api/webauthn/authenticate/verify/+server.ts +++ b/src/routes/api/webauthn/authenticate/verify/+server.ts @@ -10,6 +10,7 @@ import { checkRateLimit } from "$lib/server/ratelimit"; import { verifyPasskeyAuthentication, consumeChallenge, getWebAuthnConfig } from "$lib/server/auth/webauthn"; import type { AuthenticationResponseJSON } from "$lib/server/auth/webauthn"; import { users } from "$lib/server/db/schema"; +import { translate } from "$lib/i18n/server"; function extractChallengeFromClientData(clientDataJSONb64u: string): string | null { try { @@ -42,10 +43,10 @@ export const POST: RequestHandler = async (event) => { })() : null; if (reqOrigin && reqOrigin !== origin) { - throw error(403, "유효하지 않은 출처입니다."); + throw error(403, translate(locals.locale, "webauthn.errors.invalid_origin")); } if (!reqOrigin && refOrigin && refOrigin !== origin) { - throw error(403, "유효하지 않은 출처입니다."); + throw error(403, translate(locals.locale, "webauthn.errors.invalid_origin")); } const body = (await request.json()) as AuthenticationResponseJSON & { _redirectTo?: string }; @@ -58,17 +59,17 @@ export const POST: RequestHandler = async (event) => { const rlKey = `webauthn-verify:${tenant.id}:${ipKey}`; const rl = await checkRateLimit(db, rlKey, { windowMs: 5 * 60 * 1000, limit: 10 }); if (!rl.allowed) { - throw error(429, "인증 시도가 너무 많습니다. 잠시 후 다시 시도해 주세요."); + throw error(429, translate(locals.locale, "webauthn.errors.auth_rate_limited")); } // 1회용 challenge 소진 (DB 기반) const clientChallenge = body.response?.clientDataJSON ? extractChallengeFromClientData(body.response.clientDataJSON) : null; if (!clientChallenge) { - throw error(400, "인증 세션이 유효하지 않습니다."); + throw error(400, translate(locals.locale, "webauthn.errors.auth_session_invalid")); } const challengeOk = await consumeChallenge(db, tenant.id, clientChallenge); if (!challengeOk) { - throw error(400, "인증 세션이 만료되었거나 이미 사용되었습니다."); + throw error(400, translate(locals.locale, "webauthn.errors.auth_session_expired")); } const result = await verifyPasskeyAuthentication(db, body, clientChallenge, rpID, origin, tenant.id); @@ -83,18 +84,18 @@ export const POST: RequestHandler = async (event) => { userAgent: requestMetadata.userAgent, detail: { method: "webauthn" }, }); - throw error(400, "패스키 인증에 실패했습니다."); + throw error(400, translate(locals.locale, "webauthn.errors.auth_failed")); } // 사용자 조회 const [user] = await db.select().from(users).where(eq(users.id, result.userId)).limit(1); if (!user || user.status !== "active") { - throw error(403, "비활성화된 계정입니다."); + throw error(403, translate(locals.locale, "webauthn.errors.account_disabled")); } if (user.tenantId !== tenant.id) { - throw error(403, "접근 권한이 없습니다."); + throw error(403, translate(locals.locale, "webauthn.errors.access_denied")); } const requestMetadata = getRequestMetadata(event); diff --git a/src/routes/api/webauthn/passkey-client/+server.ts b/src/routes/api/webauthn/passkey-client/+server.ts index 10b17f2..4e96d12 100644 --- a/src/routes/api/webauthn/passkey-client/+server.ts +++ b/src/routes/api/webauthn/passkey-client/+server.ts @@ -1,6 +1,11 @@ import type { RequestHandler } from "@sveltejs/kit"; +import { translate } from "$lib/i18n/server"; +import type { Locale } from "$lib/i18n/core"; -const script = ` +// 클라이언트로 내려가는 스크립트는 서버에서 locale 별 사용자 문구를 주입해 생성한다. +// 사용자 대면 문자열(취소/실패)은 translate 로 해석한 뒤 JSON.stringify 로 안전하게 임베드한다. +function buildScript(cancelledMsg: string, failedMsg: string): string { + return ` (function(){ function b64uToBuf(b64u){ var b64=b64u.replace(/-/g,'+').replace(/_/g,'/'); @@ -42,7 +47,7 @@ const script = ` opts.challenge=b64uToBuf(opts.challenge); if(opts.allowCredentials)opts.allowCredentials=opts.allowCredentials.map(function(c){return Object.assign({},c,{id:b64uToBuf(c.id)});}); var assertion=await navigator.credentials.get({publicKey:opts}); - if(!assertion)throw new Error('패스키 인증이 취소되었습니다.'); + if(!assertion)throw new Error(${JSON.stringify(cancelledMsg)}); var body={ id:assertion.id,rawId:bufToB64u(assertion.rawId),type:assertion.type, response:{ @@ -66,7 +71,7 @@ const script = ` window.location.href=dest; }catch(e){ btn.disabled=false;btn.textContent=orig; - showError(e&&e.message?e.message:'패스키 인증에 실패했습니다.'); + showError(e&&e.message?e.message:${JSON.stringify(failedMsg)}); } } // ctrls H-FRONT-2: redirectTo 를 더 이상 DOM input 에서 읽지 않는다. @@ -102,11 +107,17 @@ const script = ` } })(); `; +} -export const GET: RequestHandler = () => - new Response(script, { +export const GET: RequestHandler = ({ locals }) => { + const locale: Locale = locals.locale; + const script = buildScript(translate(locale, "webauthn.errors.auth_cancelled"), translate(locale, "webauthn.errors.auth_failed")); + return new Response(script, { headers: { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "public, max-age=3600", + // locale 별로 사용자 문구가 달라지므로 캐시 키를 locale 결정 입력(쿠키/Accept-Language)으로 분리. + Vary: "Cookie, Accept-Language", }, }); +}; diff --git a/src/routes/api/webauthn/register/options/+server.ts b/src/routes/api/webauthn/register/options/+server.ts index a419151..3c4ec04 100644 --- a/src/routes/api/webauthn/register/options/+server.ts +++ b/src/routes/api/webauthn/register/options/+server.ts @@ -3,16 +3,17 @@ import type { RequestHandler } from "./$types"; import { requireDbContext } from "$lib/server/auth/guards"; import { getRuntimeConfig } from "$lib/server/auth/runtime"; import { buildRegistrationOptions, createChallengeCookie, getWebAuthnConfig, WEBAUTHN_CHALLENGE_COOKIE } from "$lib/server/auth/webauthn"; +import { translate } from "$lib/i18n/server"; export const POST: RequestHandler = async (event) => { const { locals, cookies, url, platform } = event; if (!locals.user) { - throw error(401, "로그인이 필요합니다."); + throw error(401, translate(locals.locale, "webauthn.errors.login_required")); } const config = getRuntimeConfig(platform); if (!config.signingKeySecret) { - throw error(503, "IDP_SIGNING_KEY_SECRET 이 설정되지 않았습니다."); + throw error(503, translate(locals.locale, "webauthn.errors.signing_key_not_configured")); } const { db } = requireDbContext(locals); diff --git a/src/routes/oidc/authorize/+server.ts b/src/routes/oidc/authorize/+server.ts index 543c346..0491f00 100644 --- a/src/routes/oidc/authorize/+server.ts +++ b/src/routes/oidc/authorize/+server.ts @@ -8,6 +8,7 @@ import { checkRateLimit } from "$lib/server/ratelimit"; import { hasServiceAccess } from "$lib/server/access/service-permissions"; import { verifyIdToken } from "$lib/server/crypto/keys"; import { resolveIssuerUrl } from "$lib/server/auth/runtime"; +import { translate } from "$lib/i18n/server"; /** redirect_uri 가 확정된 이후에만 사용. 그 전 오류는 throw error() 로 직접 응답. */ function authRedirectError(redirectUri: string, errorCode: string, description: string, state?: string | null): never { @@ -29,7 +30,7 @@ export const GET: RequestHandler = async (event) => { limit: 60, }); if (!rl.allowed) { - throw error(429, "요청이 너무 많습니다. 잠시 후 다시 시도해 주세요."); + throw error(429, translate(locals.locale, "oidc.errors.rate_limited")); } const clientId = url.searchParams.get("client_id"); @@ -43,19 +44,19 @@ export const GET: RequestHandler = async (event) => { // client_id / redirect_uri 가 없으면 redirect 불가 → 직접 오류 응답 if (!clientId || !redirectUri) { - throw error(400, "client_id 와 redirect_uri 는 필수입니다."); + throw error(400, translate(locals.locale, "oidc.errors.client_id_redirect_uri_required")); } if (responseType !== "code") { - throw error(400, "response_type=code 만 지원합니다."); + throw error(400, translate(locals.locale, "oidc.errors.response_type_unsupported")); } const client = await findOidcClient(db, tenant.id, clientId); if (!client) { - throw error(401, "등록되지 않은 client_id 입니다."); + throw error(401, translate(locals.locale, "oidc.errors.unknown_client")); } if (!isAllowedRedirectUri(client, redirectUri)) { - throw error(400, "redirect_uri 가 등록된 값과 일치하지 않습니다."); + throw error(400, translate(locals.locale, "oidc.errors.redirect_uri_mismatch")); } // PKCE 검증 @@ -75,7 +76,7 @@ export const GET: RequestHandler = async (event) => { userAgent: getRequestMetadata(event).userAgent, detail: { error: "invalid_request", reason: "pkce_required" }, }); - authRedirectError(redirectUri, "invalid_request", "PKCE code_challenge 가 필요합니다.", state); + authRedirectError(redirectUri, "invalid_request", translate(locals.locale, "oidc.errors.pkce_required"), state); } if (codeChallenge !== null && !hasChallenge) { await recordAuditEvent(db, { @@ -87,7 +88,7 @@ export const GET: RequestHandler = async (event) => { userAgent: getRequestMetadata(event).userAgent, detail: { error: "invalid_request", reason: "empty_code_challenge" }, }); - authRedirectError(redirectUri, "invalid_request", "code_challenge 가 비어 있습니다.", state); + authRedirectError(redirectUri, "invalid_request", translate(locals.locale, "oidc.errors.empty_code_challenge"), state); } if (hasChallenge && codeChallengeMethod !== "S256") { await recordAuditEvent(db, { @@ -99,7 +100,7 @@ export const GET: RequestHandler = async (event) => { userAgent: getRequestMetadata(event).userAgent, detail: { error: "invalid_request", reason: "invalid_code_challenge_method", method: codeChallengeMethod }, }); - authRedirectError(redirectUri, "invalid_request", "code_challenge_method=S256 만 지원합니다.", state); + authRedirectError(redirectUri, "invalid_request", translate(locals.locale, "oidc.errors.code_challenge_method_unsupported"), state); } // scope 검증 @@ -114,7 +115,7 @@ export const GET: RequestHandler = async (event) => { userAgent: getRequestMetadata(event).userAgent, detail: { error: "invalid_scope", requestedScope: scope }, }); - authRedirectError(redirectUri, "invalid_scope", "openid scope 가 필요합니다.", state); + authRedirectError(redirectUri, "invalid_scope", translate(locals.locale, "oidc.errors.openid_scope_required"), state); } const grantedScope = grantedScopes.join(" "); @@ -123,7 +124,7 @@ export const GET: RequestHandler = async (event) => { const promptNone = prompts.has("none"); // prompt=none 은 다른 값과 함께 올 수 없다. if (promptNone && prompts.size > 1) { - authRedirectError(redirectUri, "invalid_request", "prompt=none 은 다른 prompt 값과 함께 쓸 수 없습니다.", state); + authRedirectError(redirectUri, "invalid_request", translate(locals.locale, "oidc.errors.prompt_none_conflict"), state); } const maxAgeRaw = url.searchParams.get("max_age"); @@ -131,7 +132,7 @@ export const GET: RequestHandler = async (event) => { if (maxAgeRaw !== null) { const n = Number.parseInt(maxAgeRaw, 10); if (!Number.isFinite(n) || n < 0) { - authRedirectError(redirectUri, "invalid_request", "max_age 는 음이 아닌 정수여야 합니다.", state); + authRedirectError(redirectUri, "invalid_request", translate(locals.locale, "oidc.errors.max_age_invalid"), state); } maxAge = n; } @@ -172,7 +173,7 @@ export const GET: RequestHandler = async (event) => { userAgent: getRequestMetadata(event).userAgent, detail: { error: "login_required", reason: reauthRequired ? "reauth_required" : "not_authenticated" }, }); - authRedirectError(redirectUri, "login_required", "사용자 상호작용 없이 인증을 완료할 수 없습니다.", state); + authRedirectError(redirectUri, "login_required", translate(locals.locale, "oidc.errors.login_required"), state); } if (needsInteraction) { @@ -187,7 +188,7 @@ export const GET: RequestHandler = async (event) => { // 여기 도달 시 로그인 상태가 보장된다 (타입 좁히기용 방어 체크). if (!locals.user || !locals.session) { - throw error(500, "인증 게이트 이후 세션이 없습니다."); + throw error(500, translate(locals.locale, "oidc.errors.session_missing_after_gate")); } // 서비스 권한 게이트 (기본 deny). 매핑 없으면 SSO 거부. @@ -209,7 +210,7 @@ export const GET: RequestHandler = async (event) => { userAgent: getRequestMetadata(event).userAgent, detail: { error: "access_denied", reason: "no_service_assignment" }, }); - authRedirectError(redirectUri, "access_denied", "이 서비스에 대한 권한이 없습니다.", state); + authRedirectError(redirectUri, "access_denied", translate(locals.locale, "oidc.errors.service_access_denied"), state); } // authorization code 발급 diff --git a/src/routes/oidc/revoke/+server.ts b/src/routes/oidc/revoke/+server.ts index 52a7ab8..6ac57f9 100644 --- a/src/routes/oidc/revoke/+server.ts +++ b/src/routes/oidc/revoke/+server.ts @@ -14,6 +14,7 @@ import { getRequestMetadata } from "$lib/server/audit"; import { checkRateLimit } from "$lib/server/ratelimit"; import { authenticateOidcClient } from "$lib/server/oidc/client"; import { revokeRefreshTokenByValue } from "$lib/server/oidc/refresh"; +import { translate } from "$lib/i18n/server"; function errorResponse(code: string, description: string, status: number): Response { return new Response(JSON.stringify({ error: code, error_description: description }), { @@ -29,7 +30,7 @@ export const POST: RequestHandler = async (event) => { const { ipKey } = getRequestMetadata(event); const rl = await checkRateLimit(db, `oidc-revoke:${ipKey}`, { windowMs: 60 * 1000, limit: 60 }); if (!rl.allowed) { - return new Response(JSON.stringify({ error: "rate_limit_exceeded", error_description: "요청이 너무 많습니다." }), { + return new Response(JSON.stringify({ error: "rate_limit_exceeded", error_description: translate(locals.locale, "oidc.errors.rate_limited_short") }), { status: 429, headers: { "Content-Type": "application/json", "Retry-After": String(Math.ceil(rl.retryAfterMs / 1000)) }, }); @@ -41,7 +42,7 @@ export const POST: RequestHandler = async (event) => { const token = String(body.get("token") ?? ""); // RFC 7009 §2.1: token 이 없으면 invalid_request. 그 외(무효/타 클라이언트 토큰)는 200. - if (!token) return errorResponse("invalid_request", "token 파라미터가 필요합니다.", 400); + if (!token) return errorResponse("invalid_request", translate(locals.locale, "oidc.errors.token_param_required"), 400); const hint = String(body.get("token_type_hint") ?? ""); // access_token 힌트가 명시된 경우 refresh 폐기를 건너뛴다(무의미한 조회 회피). diff --git a/src/routes/saml/metadata/+server.ts b/src/routes/saml/metadata/+server.ts index 11d39f6..700f82a 100644 --- a/src/routes/saml/metadata/+server.ts +++ b/src/routes/saml/metadata/+server.ts @@ -4,13 +4,14 @@ import { requireDbContext } from "$lib/server/auth/guards"; import { getRuntimeConfig } from "$lib/server/auth/runtime"; import { generateIdpMetadataXml } from "$lib/server/saml/metadata"; import { findSp } from "$lib/server/saml/sp"; +import { translate } from "$lib/i18n/server"; export const GET: RequestHandler = async ({ locals, platform, url }) => { const { db, tenant } = requireDbContext(locals); const config = getRuntimeConfig(platform); if (!config.issuerUrl) { - throw error(503, "IDP_ISSUER_URL 미설정"); + throw error(503, translate(locals.locale, "saml.errors.issuer_not_set")); } // SP 컨텍스트가 주어지면(`?sp=`) 해당 SP의 wantAuthnRequestsSigned 를 광고에 반영. diff --git a/test/unit/admin-zod.test.ts b/test/unit/admin-zod.test.ts index 409e406..8e7bfa4 100644 --- a/test/unit/admin-zod.test.ts +++ b/test/unit/admin-zod.test.ts @@ -38,7 +38,7 @@ describe("teamCreateSchema", () => { it("name 이 비면 실패 + 친화적 메시지", () => { const res = teamCreateSchema.safeParse({ name: " " }); expect(res.success).toBe(false); - if (!res.success) expect(res.error.issues[0]?.message).toBe("팀명을 입력해 주세요."); + if (!res.success) expect(res.error.issues[0]?.message).toBe("admin.errors.team_name_required"); }); it("departmentId 가 지정되면 trim 값 유지", () => { @@ -51,7 +51,7 @@ describe("teamUpdateSchema", () => { it("id/name 없으면 실패 (잘못된 요청)", () => { const res = teamUpdateSchema.safeParse({ name: "팀", status: "active" }); expect(res.success).toBe(false); - if (!res.success) expect(res.error.issues[0]?.message).toBe("잘못된 요청입니다."); + if (!res.success) expect(res.error.issues[0]?.message).toBe("admin.errors.invalid_request"); }); it("status 는 active/inactive 만 허용 (잘못된 enum 거부)", () => { @@ -82,7 +82,7 @@ describe("positionCreateSchema / positionUpdateSchema (level 정수 coerce)", () it("level 이 숫자가 아니면 실패", () => { const res = positionCreateSchema.safeParse({ name: "과장", level: "abc" }); expect(res.success).toBe(false); - if (!res.success) expect(res.error.issues[0]?.message).toBe("레벨은 숫자여야 합니다."); + if (!res.success) expect(res.error.issues[0]?.message).toBe("admin.errors.level_must_be_number"); }); it("update 도 id/name 필수 + level coerce", () => { diff --git a/test/unit/crud-factory.test.ts b/test/unit/crud-factory.test.ts index 4c09dde..4c7d306 100644 --- a/test/unit/crud-factory.test.ts +++ b/test/unit/crud-factory.test.ts @@ -44,7 +44,7 @@ function makeEvent(db: unknown, form: Record): RequestEvent { const fd = new FormData(); for (const [k, v] of Object.entries(form)) fd.set(k, v); return { - locals: { db, tenant: { id: "tenant-1" }, user: { id: "user-1", role: "admin" } }, + locals: { db, tenant: { id: "tenant-1" }, user: { id: "user-1", role: "admin" }, locale: "ko" }, request: { formData: async () => fd, headers: new Headers() }, } as unknown as RequestEvent; } From dc485bfcf9c3cee776ec7ee3f70fcd14c376797e Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 13:46:58 +0900 Subject: [PATCH 14/26] =?UTF-8?q?test:=20P11=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=E2=80=94=20=ED=9B=84=EC=86=8D=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B4=EB=93=9C=20=ED=94=8C=EB=A1=9C=EC=9A=B0=20E2E?= =?UTF-8?q?=20=EC=BB=A4=EB=B2=84=EB=A6=AC=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - account-deletion·invite-accept·secret-rotation·session-refresh-cascade·oidc-flow 통합 테스트 - test/stubs/app-environment: 통합 하니스용 App.Platform/환경 스텁, vitest.config 에 integration 포함 Co-Authored-By: Claude Fable 5 --- test/integration/account-deletion.test.ts | 112 +++++++ test/integration/harness.ts | 313 ++++++++++++++++++ test/integration/invite-accept.test.ts | 98 ++++++ test/integration/oidc-flow.test.ts | 193 +++++++++++ test/integration/secret-rotation.test.ts | 86 +++++ .../session-refresh-cascade.test.ts | 76 +++++ test/stubs/app-environment.ts | 16 + vitest.config.ts | 3 + 8 files changed, 897 insertions(+) create mode 100644 test/integration/account-deletion.test.ts create mode 100644 test/integration/harness.ts create mode 100644 test/integration/invite-accept.test.ts create mode 100644 test/integration/oidc-flow.test.ts create mode 100644 test/integration/secret-rotation.test.ts create mode 100644 test/integration/session-refresh-cascade.test.ts create mode 100644 test/stubs/app-environment.ts diff --git a/test/integration/account-deletion.test.ts b/test/integration/account-deletion.test.ts new file mode 100644 index 0000000..1df725f --- /dev/null +++ b/test/integration/account-deletion.test.ts @@ -0,0 +1,112 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { eq } from "drizzle-orm"; +import { actions as loginActions } from "../../src/routes/(auth)/login/+page.server"; +import { runExpiredDataGc } from "../../src/lib/server/db/gc"; +import { users } from "../../src/lib/server/db/schema"; +import { openMemoryDb, seedTenantAndSigningKey, seedUser, makeEvent, catchRedirect, TEST_ISSUER_URL, type MemoryDb } from "./harness"; +import type { Tenant, User } from "../../src/lib/server/db/schema"; + +// Phase 8: 셀프서비스 계정 삭제(deletion_pending) → 유예 내 로그인 복구, 유예 경과 시 GC 하드삭제. +// 삭제 신청 자체는 세션+step-up 재인증이 얽혀 있어, DB 상태(deletion_pending)를 직접 구성하고 +// 실제 login 액션(복구 분기)과 실제 GC 하드삭제 조건을 검증한다(가짜 통과 없이 실 로직 구동). + +let mem: MemoryDb; +let tenant: Tenant; + +beforeEach(async () => { + mem = await openMemoryDb(); + tenant = await seedTenantAndSigningKey(mem); +}); + +afterEach(() => mem.close()); + +async function statusOf(userId: string): Promise { + const [u] = await mem.db.select().from(users).where(eq(users.id, userId)).limit(1); + return u?.status; +} + +describe("Phase 8 — 계정 삭제 신청/복구/GC", () => { + it("유예 기간 내 deletion_pending 계정은 로그인(recover=1)으로 active 복구된다", async () => { + const user: User = await seedUser(mem.db, { + tenantId: tenant.id, + email: "carol@test.example", + username: "carol", + password: "carol-password", + status: "deletion_pending", + deletionScheduledAt: new Date(Date.now() + 20 * 24 * 60 * 60 * 1000), // 유예 내(20일 후 예정) + }); + + // 1단계: recover 없이 로그인 → 복구 확인 프롬프트 반환(세션 생성/복구 없음) + const step1 = (await loginActions.default( + makeEvent({ + method: "POST", + url: `${TEST_ISSUER_URL}/login`, + form: { username: "carol", password: "carol-password" }, + locals: { db: mem.db, tenant, env: mem.env }, + }), + )) as { recovery?: boolean }; + expect(step1.recovery).toBe(true); + expect(await statusOf(user.id)).toBe("deletion_pending"); // 아직 복구 안 됨 + + // 2단계: recover=1 로 재제출 → 복구 후 정상 로그인(리다이렉트) + const redirect = await catchRedirect(() => + loginActions.default( + makeEvent({ + method: "POST", + url: `${TEST_ISSUER_URL}/login`, + form: { username: "carol", password: "carol-password", recover: "1" }, + locals: { db: mem.db, tenant, env: mem.env }, + }), + ), + ); + expect(redirect.status).toBe(303); + expect(await statusOf(user.id)).toBe("active"); + const [restored] = await mem.db.select().from(users).where(eq(users.id, user.id)).limit(1); + expect(restored.deletionScheduledAt).toBeNull(); + }); + + it("유예 경과한 deletion_pending 계정은 복구가 거부되고(400), GC 가 하드 삭제한다", async () => { + const elapsed: User = await seedUser(mem.db, { + tenantId: tenant.id, + email: "dave@test.example", + username: "dave", + password: "dave-password", + status: "deletion_pending", + deletionScheduledAt: new Date(Date.now() - 1000), // 유예 경과(과거) + }); + + // 로그인 복구 시도 → 유예 경과로 거부(400) + const res = (await loginActions.default( + makeEvent({ + method: "POST", + url: `${TEST_ISSUER_URL}/login`, + form: { username: "dave", password: "dave-password", recover: "1" }, + locals: { db: mem.db, tenant, env: mem.env }, + }), + )) as { status?: number }; + expect(res.status).toBe(400); + expect(await statusOf(elapsed.id)).toBe("deletion_pending"); // 여전히 존재 + + // GC 실행 → 유예 경과 deletion_pending 만 하드 삭제 + const result = await runExpiredDataGc(mem.db); + const usersGc = result.tables.find((t) => t.table === "users"); + expect(usersGc?.ok).toBe(true); + expect(await statusOf(elapsed.id)).toBeUndefined(); // 하드 삭제됨 + }); + + it("GC 는 활성 계정과 유예 미경과 계정을 삭제하지 않는다(보수적 조건)", async () => { + const active = await seedUser(mem.db, { tenantId: tenant.id, email: "erin@test.example", username: "erin", status: "active" }); + const pendingFuture = await seedUser(mem.db, { + tenantId: tenant.id, + email: "frank@test.example", + username: "frank", + status: "deletion_pending", + deletionScheduledAt: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), + }); + + await runExpiredDataGc(mem.db); + + expect(await statusOf(active.id)).toBe("active"); + expect(await statusOf(pendingFuture.id)).toBe("deletion_pending"); + }); +}); diff --git a/test/integration/harness.ts b/test/integration/harness.ts new file mode 100644 index 0000000..acb3faf --- /dev/null +++ b/test/integration/harness.ts @@ -0,0 +1,313 @@ +/** + * Phase 11 통합 테스트 하네스. + * + * 목적: mock 이 아닌 **실제 DB**(libSQL `:memory:`) 위에 **실제 drizzle 스키마 마이그레이션**을 + * 적용하고, 실제 서버 모듈(auth/oidc/route 핸들러)을 직접 구동해 풀플로우를 검증한다. + * + * 핵심 구성: + * 1. openMemoryDb(): libSQL `:memory:` 클라이언트 + drizzle 인스턴스를 만들고 + * `drizzle/sqlite/*.sql` 을 journal 순서대로 적용한다(scripts/lib/db.ts 의 executeMultiple 패턴). + * 2. makeEvent(): 라우트 핸들러(+server.ts / +page.server.ts)가 기대하는 RequestEvent/locals 를 + * 최소 구성으로 조립한다(db·tenant·session·user·runtimeConfig·platform·cookies·locale). + * 3. 시드 유틸: 테넌트/서명키/유저/OIDC 클라이언트/서비스 할당/세션. + * + * 격리: 케이스마다 새 인메모리 DB(= 새 tenantId)를 만든다. 서명키/JWKS/baseline 은 + * globalThis 캐시를 쓰므로 tenantId 가 달라 케이스 간 오염이 없다(추가로 서명키 캐시는 + * invalidate 유틸을 노출한다). + */ + +import { fileURLToPath } from "node:url"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { createClient, type Client } from "@libsql/client"; +import { drizzle } from "drizzle-orm/libsql"; +import { eq } from "drizzle-orm"; +import type { RequestEvent } from "@sveltejs/kit"; +import type { DB } from "$lib/server/db"; +import { credentials, oidcClients, sessions, tenants, userServiceAssignments, users, type Session, type Tenant, type User } from "$lib/server/db/schema"; +import { ensureDefaultTenant, ensureSigningKey } from "$lib/server/auth/bootstrap"; +import { getRuntimeConfig, type RuntimeConfig } from "$lib/server/auth/runtime"; +import { hashPassword } from "$lib/server/auth/password"; +import { hashClientSecret } from "$lib/server/oidc/client"; +import { createSessionRecord } from "$lib/server/auth/session"; +import { b64uEncode } from "$lib/server/crypto/keys"; + +// ── 테스트용 마스터 시크릿 / issuer ──────────────────────────────────────────────── +// 실 서명·암호화 경로가 이 값을 사용한다. platform.env 로 주입해 getRuntimeConfig 가 읽는다. +export const TEST_SIGNING_SECRET = "test-signing-secret-current-abcdefghijklmnopqrstuvwxyz-0123456789"; +export const TEST_SIGNING_SECRET_PREVIOUS = "test-signing-secret-previous-ABCDEFGHIJKLMNOPQRSTUVWXYZ-9876543210"; +export const TEST_ISSUER_URL = "https://idp.test.example"; + +const projectRoot = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); +const migrationsDir = join(projectRoot, "drizzle", "sqlite"); + +interface JournalEntry { + idx: number; + tag: string; +} + +/** _journal.json 을 읽어 마이그레이션 태그를 idx 오름차순으로 반환한다. */ +function migrationTags(): string[] { + const journalPath = join(migrationsDir, "meta", "_journal.json"); + const journal = JSON.parse(readFileSync(journalPath, "utf-8")) as { entries: JournalEntry[] }; + return [...journal.entries].sort((a, b) => a.idx - b.idx).map((e) => e.tag); +} + +export interface MemoryDb { + db: DB; + client: Client; + /** platform.env 로 넘길 환경. getRuntimeConfig 가 읽는다. */ + env: Record; + close(): void; +} + +/** + * libSQL `:memory:` DB 를 만들고 실제 마이그레이션(SQL)을 순서대로 적용한다. + * executeMultiple 은 `;` 로 구분된 여러 문장을 한 번에 실행한다(마이그레이션 파일 통째 적용). + */ +export async function openMemoryDb(envOverrides: Record = {}): Promise { + const client = createClient({ url: ":memory:" }); + for (const tag of migrationTags()) { + const sqlText = readFileSync(join(migrationsDir, `${tag}.sql`), "utf-8"); + await client.executeMultiple(sqlText); + } + // 앱 런타임과 동일하게 FK 제약을 활성화한다(마이그레이션 DDL 적용 이후). + await client.execute("PRAGMA foreign_keys = ON"); + + const db = drizzle(client, { schema: { tenants, users, credentials, sessions, oidcClients, userServiceAssignments } }) as unknown as DB; + + const env: Record = { + IDP_ISSUER_URL: TEST_ISSUER_URL, + IDP_SIGNING_KEY_SECRET: TEST_SIGNING_SECRET, + IDP_DEFAULT_TENANT_NAME: "Test Tenant", + ...envOverrides, + }; + + return { + db, + client, + env, + close() { + client.close(); + }, + }; +} + +// ── 테스트용 platform / RequestEvent ──────────────────────────────────────────────── + +/** getRuntimeConfig 가 읽는 최소 platform. ctx 는 없음(=Node 경로, waitUntil 미노출). */ +export function makePlatform(env: Record): App.Platform { + return { env } as unknown as App.Platform; +} + +/** Map 기반 최소 Cookies 스텁. 라우트가 set/get/delete 하는 세션·MFA 쿠키를 담는다. */ +export function makeCookies(initial: Record = {}) { + const store = new Map(Object.entries(initial)); + return { + get: (name: string) => store.get(name), + getAll: () => Array.from(store.entries()).map(([name, value]) => ({ name, value })), + set: (name: string, value: string) => { + store.set(name, value); + }, + delete: (name: string) => { + store.delete(name); + }, + serialize: () => "", + _store: store, + }; +} + +export interface MakeEventOptions { + method?: string; + url?: string; + headers?: Record; + /** application/x-www-form-urlencoded 폼 본문(POST 액션/토큰 엔드포인트용). */ + form?: Record; + locals: { + db: DB; + tenant: Tenant | null; + user?: User | null; + session?: Session | null; + locale?: string; + env: Record; + }; + cookies?: ReturnType; +} + +/** + * 실제 라우트 핸들러가 소비하는 RequestEvent 를 조립한다. + * 핸들러가 실제로 접근하는 필드(request/url/locals/platform/cookies/getClientAddress)만 채운다. + * + * 반환 타입은 `RequestEvent` 로 둔다. 각 라우트 핸들러는 `./$types` 로 + * RouteId/RouteParams 가 좁혀진 `RequestEvent` 등을 파라미터로 + * 받는데, `never` 는 모든 리터럴에 대입 가능하므로 어떤 라우트 핸들러에도 그대로 넘길 수 있다. + */ +export function makeEvent(opts: MakeEventOptions): RequestEvent { + const method = opts.method ?? "GET"; + const url = new URL(opts.url ?? "https://idp.test.example/"); + const headers = new Headers(opts.headers ?? {}); + + let body: BodyInit | undefined; + if (opts.form) { + const params = new URLSearchParams(opts.form); + body = params; + if (!headers.has("content-type")) headers.set("content-type", "application/x-www-form-urlencoded"); + } + const request = new Request(url.toString(), { method, headers, body }); + + const platform = makePlatform(opts.locals.env); + const runtimeConfig: RuntimeConfig = getRuntimeConfig(platform); + const cookies = opts.cookies ?? makeCookies(); + + const locals: App.Locals = { + db: opts.locals.db, + tenant: opts.locals.tenant, + user: opts.locals.user ?? null, + session: opts.locals.session ?? null, + runtimeConfig, + runtimeError: null, + locale: (opts.locals.locale ?? "ko") as App.Locals["locale"], + }; + + return { + request, + url, + locals, + platform, + cookies, + params: {}, + route: { id: null }, + getClientAddress: () => "127.0.0.1", + setHeaders: () => {}, + isDataRequest: false, + isSubRequest: false, + fetch: globalThis.fetch, + } as unknown as RequestEvent; +} + +// ── 시드 유틸 ──────────────────────────────────────────────────────────────────── + +/** 기본 테넌트 + 활성 서명키(실 RSA 키·cert)를 생성한다. */ +export async function seedTenantAndSigningKey(mem: MemoryDb): Promise { + const platform = makePlatform(mem.env); + const config = getRuntimeConfig(platform); + const tenant = await ensureDefaultTenant(mem.db, platform); + await ensureSigningKey(mem.db, tenant, config.signingKeySecrets, config.issuerUrl); + return tenant; +} + +export interface SeedUserOptions { + tenantId: string; + email: string; + username?: string | null; + password?: string; + role?: "admin" | "user"; + status?: "active" | "disabled" | "locked" | "deletion_pending"; + emailVerifiedAt?: Date | null; + displayName?: string; + deletionScheduledAt?: Date | null; +} + +/** 유저를 생성하고, password 가 주어지면 password credential 도 함께 생성한다. */ +export async function seedUser(db: DB, opts: SeedUserOptions): Promise { + const id = crypto.randomUUID(); + await db.insert(users).values({ + id, + tenantId: opts.tenantId, + email: opts.email.toLowerCase(), + username: opts.username === undefined ? opts.email.split("@")[0] : opts.username, + displayName: opts.displayName ?? "Test User", + role: opts.role ?? "user", + status: opts.status ?? "active", + emailVerifiedAt: opts.emailVerifiedAt === undefined ? new Date() : opts.emailVerifiedAt, + deletionScheduledAt: opts.deletionScheduledAt ?? null, + }); + + if (opts.password) { + await db.insert(credentials).values({ + id: crypto.randomUUID(), + userId: id, + type: "password", + secret: await hashPassword(opts.password), + label: "비밀번호", + }); + } + + const [row] = await db.select().from(users).where(eq(users.id, id)).limit(1); + return row!; +} + +export interface SeedOidcClientOptions { + tenantId: string; + clientId: string; + secret?: string; + redirectUris: string[]; + scopes?: string; + grantTypes?: string; + tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | "none"; + requirePkce?: boolean; +} + +export async function seedOidcClient(db: DB, opts: SeedOidcClientOptions): Promise { + const id = crypto.randomUUID(); + await db.insert(oidcClients).values({ + id, + tenantId: opts.tenantId, + clientId: opts.clientId, + clientSecretHash: opts.secret ? await hashClientSecret(opts.secret) : null, + name: opts.clientId, + redirectUris: JSON.stringify(opts.redirectUris), + scopes: opts.scopes ?? "openid profile email offline_access", + grantTypes: opts.grantTypes ?? "authorization_code,refresh_token", + tokenEndpointAuthMethod: opts.tokenEndpointAuthMethod ?? "client_secret_basic", + requirePkce: opts.requirePkce ?? true, + enabled: true, + }); + const [row] = await db.select().from(oidcClients).where(eq(oidcClients.id, id)).limit(1); + return row!; +} + +/** 유저에게 서비스(OIDC/SAML) 접근 권한을 부여한다(기본 deny 를 통과시키기 위함). */ +export async function seedServiceAssignment(db: DB, args: { tenantId: string; userId: string; serviceType: "oidc" | "saml"; serviceRefId: string }): Promise { + await db.insert(userServiceAssignments).values({ + id: crypto.randomUUID(), + tenantId: args.tenantId, + userId: args.userId, + serviceType: args.serviceType, + serviceRefId: args.serviceRefId, + }); +} + +/** 실제 createSessionRecord 로 세션을 만들고 raw 토큰 + Session row 를 돌려준다. */ +export async function seedSession(db: DB, args: { tenantId: string; userId: string }): Promise<{ session: Session; sessionToken: string }> { + const { sessionToken, sessionId } = await createSessionRecord(db, { + tenantId: args.tenantId, + userId: args.userId, + amr: ["pwd"], + acr: "urn:mace:incommon:iap:silver", + }); + const [session] = await db.select().from(sessions).where(eq(sessions.id, sessionId)).limit(1); + return { session: session!, sessionToken }; +} + +// ── 헬퍼: PKCE / redirect 파싱 ───────────────────────────────────────────────────── + +/** S256 code_challenge 를 계산한다(실 라우트/pkce.ts 와 동일 알고리즘). */ +export async function pkceChallengeS256(verifier: string): Promise { + const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)); + return b64uEncode(hash); +} + +/** throw 된 SvelteKit redirect 를 잡아 Location 을 반환한다. redirect 가 아니면 재throw. */ +export async function catchRedirect(fn: () => unknown): Promise<{ status: number; location: string }> { + try { + await fn(); + } catch (e) { + const r = e as { status?: number; location?: string }; + if (typeof r.status === "number" && typeof r.location === "string") { + return { status: r.status, location: r.location }; + } + throw e; + } + throw new Error("redirect 가 발생하지 않았습니다."); +} diff --git a/test/integration/invite-accept.test.ts b/test/integration/invite-accept.test.ts new file mode 100644 index 0000000..a929159 --- /dev/null +++ b/test/integration/invite-accept.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { eq } from "drizzle-orm"; +import { actions as acceptInviteActions } from "../../src/routes/(auth)/accept-invite/+page.server"; +import { generateToken } from "../../src/lib/server/email"; +import { authenticateLocalUser } from "../../src/lib/server/auth/users"; +import { credentials, inviteTokens, users } from "../../src/lib/server/db/schema"; +import { openMemoryDb, seedTenantAndSigningKey, seedUser, makeEvent, TEST_ISSUER_URL, type MemoryDb } from "./harness"; +import type { Tenant, User } from "../../src/lib/server/db/schema"; + +// Phase 7: 초대 수락 → credential(비밀번호) 생성 + emailVerifiedAt 세팅 + 토큰 소진 → 로그인 가능. +// 실제 accept-invite default 액션과 authenticateLocalUser 를 실 DB 로 검증한다. + +let mem: MemoryDb; +let tenant: Tenant; +let invitee: User; + +beforeEach(async () => { + mem = await openMemoryDb(); + tenant = await seedTenantAndSigningKey(mem); + // 초대 대상: 비밀번호 credential 없음 + 이메일 미인증 상태로 생성 + invitee = await seedUser(mem.db, { + tenantId: tenant.id, + email: "invitee@test.example", + username: "invitee", + emailVerifiedAt: null, + }); +}); + +afterEach(() => mem.close()); + +/** 실제 issueInvite 와 동일한 방식(generateToken → inviteTokens insert)으로 유효 초대 토큰을 만든다. */ +async function makeInvite(userId: string): Promise { + const { token, tokenHash } = await generateToken(); + await mem.db.insert(inviteTokens).values({ userId, tokenHash, expiresAt: new Date(Date.now() + 60 * 60 * 1000) }); + return token; +} + +describe("Phase 7 — 초대 수락", () => { + it("초대 수락이 password credential 생성 + emailVerifiedAt 세팅 + 토큰 소진을 원자적으로 수행하고, 이후 로그인 가능하다", async () => { + const token = await makeInvite(invitee.id); + + const event = makeEvent({ + method: "POST", + url: `${TEST_ISSUER_URL}/accept-invite`, + form: { token, password: "new-strong-password", confirmPassword: "new-strong-password" }, + locals: { db: mem.db, tenant, env: mem.env }, + }); + const result = await acceptInviteActions.default(event); + expect(result).toEqual({ accepted: true }); + + // password credential 생성 확인 + const cred = await mem.db.select().from(credentials).where(eq(credentials.userId, invitee.id)); + expect(cred.length).toBe(1); + expect(cred[0].type).toBe("password"); + expect(cred[0].secret).toBeTruthy(); + + // emailVerifiedAt 세팅 확인(초대 클릭 = 이메일 소유 증명) + const [u] = await mem.db.select().from(users).where(eq(users.id, invitee.id)).limit(1); + expect(u.emailVerifiedAt).not.toBeNull(); + + // 토큰 소진 확인 + const [tok] = await mem.db.select().from(inviteTokens).where(eq(inviteTokens.userId, invitee.id)).limit(1); + expect(tok.usedAt).not.toBeNull(); + + // 실제 로컬 인증으로 로그인 가능(설정한 비밀번호로 통과) + const authed = await authenticateLocalUser(mem.db, tenant.id, "invitee", "new-strong-password"); + expect(authed?.id).toBe(invitee.id); + }); + + it("비밀번호 확인 불일치는 400 으로 거부하고 credential 을 만들지 않는다", async () => { + const token = await makeInvite(invitee.id); + const event = makeEvent({ + method: "POST", + url: `${TEST_ISSUER_URL}/accept-invite`, + form: { token, password: "abcdefgh", confirmPassword: "different" }, + locals: { db: mem.db, tenant, env: mem.env }, + }); + const result = (await acceptInviteActions.default(event)) as { status?: number }; + expect(result.status).toBe(400); + const cred = await mem.db.select().from(credentials).where(eq(credentials.userId, invitee.id)); + expect(cred.length).toBe(0); + }); + + it("소진된 토큰은 재사용할 수 없다", async () => { + const token = await makeInvite(invitee.id); + const mkEvent = () => + makeEvent({ + method: "POST", + url: `${TEST_ISSUER_URL}/accept-invite`, + form: { token, password: "first-password-1", confirmPassword: "first-password-1" }, + locals: { db: mem.db, tenant, env: mem.env }, + }); + expect(await acceptInviteActions.default(mkEvent())).toEqual({ accepted: true }); + // 두 번째 제출: 토큰이 이미 소진됨 → invalid_link 400 + const second = (await acceptInviteActions.default(mkEvent())) as { status?: number }; + expect(second.status).toBe(400); + }); +}); diff --git a/test/integration/oidc-flow.test.ts b/test/integration/oidc-flow.test.ts new file mode 100644 index 0000000..0152872 --- /dev/null +++ b/test/integration/oidc-flow.test.ts @@ -0,0 +1,193 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { GET as authorizeGET } from "../../src/routes/oidc/authorize/+server"; +import { POST as tokenPOST } from "../../src/routes/oidc/token/+server"; +import { GET as userinfoGET } from "../../src/routes/oidc/userinfo/+server"; +import { verifyIdToken } from "../../src/lib/server/crypto/keys"; +import { + openMemoryDb, + seedTenantAndSigningKey, + seedUser, + seedOidcClient, + seedServiceAssignment, + seedSession, + makeEvent, + pkceChallengeS256, + catchRedirect, + TEST_ISSUER_URL, + type MemoryDb, +} from "./harness"; +import type { Tenant, User, Session } from "../../src/lib/server/db/schema"; + +// OIDC Authorization Code + PKCE 풀플로우를 실 DB(libSQL :memory:) + 실 라우트 핸들러로 검증한다. +// authorize(PKCE) → code → token(id_token/access_token/refresh_token) → userinfo, 그리고 code 1회 소진. + +const CLIENT_ID = "test-web-client"; +const CLIENT_SECRET = "s3cr3t-client-secret-value-0123456789"; +const REDIRECT_URI = "https://app.test.example/callback"; + +let mem: MemoryDb; +let tenant: Tenant; +let user: User; +let session: Session; +let clientDbId: string; + +beforeEach(async () => { + mem = await openMemoryDb(); + tenant = await seedTenantAndSigningKey(mem); + user = await seedUser(mem.db, { + tenantId: tenant.id, + email: "alice@test.example", + username: "alice", + password: "correct horse battery staple", + displayName: "Alice Example", + }); + const client = await seedOidcClient(mem.db, { + tenantId: tenant.id, + clientId: CLIENT_ID, + secret: CLIENT_SECRET, + redirectUris: [REDIRECT_URI], + scopes: "openid profile email offline_access", + }); + clientDbId = client.id; + await seedServiceAssignment(mem.db, { tenantId: tenant.id, userId: user.id, serviceType: "oidc", serviceRefId: clientDbId }); + const seeded = await seedSession(mem.db, { tenantId: tenant.id, userId: user.id }); + session = seeded.session; +}); + +afterEach(() => { + mem.close(); +}); + +/** authorize 를 호출하고 발급된 authorization code 를 돌려준다. */ +async function runAuthorize(verifier: string, opts: { state?: string; nonce?: string } = {}): Promise { + const challenge = await pkceChallengeS256(verifier); + const params = new URLSearchParams({ + client_id: CLIENT_ID, + redirect_uri: REDIRECT_URI, + response_type: "code", + scope: "openid profile email offline_access", + code_challenge: challenge, + code_challenge_method: "S256", + }); + if (opts.state) params.set("state", opts.state); + if (opts.nonce) params.set("nonce", opts.nonce); + + const event = makeEvent({ + method: "GET", + url: `${TEST_ISSUER_URL}/oidc/authorize?${params.toString()}`, + locals: { db: mem.db, tenant, user, session, env: mem.env }, + }); + + const { status, location } = await catchRedirect(() => authorizeGET(event)); + expect(status).toBe(302); + const dest = new URL(location); + expect(`${dest.origin}${dest.pathname}`).toBe(REDIRECT_URI); + const code = dest.searchParams.get("code"); + expect(code).toBeTruthy(); + if (opts.state) expect(dest.searchParams.get("state")).toBe(opts.state); + return code!; +} + +async function runToken(form: Record): Promise { + const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64"); + return (await tokenPOST( + makeEvent({ + method: "POST", + url: `${TEST_ISSUER_URL}/oidc/token`, + headers: { authorization: `Basic ${basic}` }, + form, + locals: { db: mem.db, tenant, env: mem.env }, + }), + )) as Response; +} + +describe("OIDC 풀플로우 (authorize → token → userinfo)", () => { + it("PKCE 인가코드 교환으로 id_token/access_token/refresh_token 을 발급하고 검증한다", async () => { + const verifier = "pkce-verifier-abcdefghijklmnopqrstuvwxyz-0123456789-ABCDEFG"; + const code = await runAuthorize(verifier, { state: "xyz-state", nonce: "nonce-123" }); + + const res = await runToken({ + grant_type: "authorization_code", + code, + redirect_uri: REDIRECT_URI, + code_verifier: verifier, + }); + expect(res.status).toBe(200); + const body = (await res.json()) as Record; + expect(body.token_type).toBe("Bearer"); + expect(body.access_token).toBeTruthy(); + expect(body.id_token).toBeTruthy(); + // offline_access 요청 + refresh_token grant 허용 → refresh token 발급 + expect(body.refresh_token).toBeTruthy(); + + // 실 서명키로 id_token 서명 검증 + 클레임 확인 + const claims = await verifyIdToken(mem.db, tenant.id, body.id_token, { expectedIssuer: TEST_ISSUER_URL, expectedAud: CLIENT_ID }); + expect(claims).not.toBeNull(); + expect(claims!.sub).toBe(user.id); + expect(claims!.iss).toBe(TEST_ISSUER_URL); + expect(claims!.aud).toBe(CLIENT_ID); + expect(claims!.nonce).toBe("nonce-123"); + expect(claims!.email).toBe("alice@test.example"); + expect(claims!.sid).toBe(session.id); + + // access token 으로 userinfo 조회 + const uinfo = (await userinfoGET( + makeEvent({ + method: "GET", + url: `${TEST_ISSUER_URL}/oidc/userinfo`, + headers: { authorization: `Bearer ${body.access_token}` }, + locals: { db: mem.db, tenant, env: mem.env }, + }), + )) as Response; + expect(uinfo.status).toBe(200); + const ui = (await uinfo.json()) as Record; + expect(ui.sub).toBe(user.id); + expect(ui.email).toBe("alice@test.example"); + expect(ui.name).toBe("Alice Example"); + }); + + it("authorization code 는 1회만 소진된다(재사용 거부)", async () => { + const verifier = "verifier-second-case-zzzzzzzzzzzzzzzzzzzzzzzzz-000111222333"; + const code = await runAuthorize(verifier); + + const first = await runToken({ grant_type: "authorization_code", code, redirect_uri: REDIRECT_URI, code_verifier: verifier }); + expect(first.status).toBe(200); + + const second = await runToken({ grant_type: "authorization_code", code, redirect_uri: REDIRECT_URI, code_verifier: verifier }); + expect(second.status).toBe(400); + const err = (await second.json()) as Record; + expect(err.error).toBe("invalid_grant"); + }); + + it("PKCE code_verifier 불일치 시 거부한다", async () => { + const verifier = "verifier-correct-value-mismatch-case-4444555566667777"; + const code = await runAuthorize(verifier); + const res = await runToken({ grant_type: "authorization_code", code, redirect_uri: REDIRECT_URI, code_verifier: "wrong-verifier-value" }); + expect(res.status).toBe(400); + expect(((await res.json()) as Record).error).toBe("invalid_grant"); + }); + + it("서비스 접근 권한 매핑이 없으면 SSO 를 거부한다(기본 deny)", async () => { + // 권한 없는 별도 유저 + 세션으로 authorize 시도 + const other = await seedUser(mem.db, { tenantId: tenant.id, email: "mallory@test.example", username: "mallory", password: "pw" }); + const otherSession = (await seedSession(mem.db, { tenantId: tenant.id, userId: other.id })).session; + const challenge = await pkceChallengeS256("verifier-denied-user-8888999900001111aaaa"); + const params = new URLSearchParams({ + client_id: CLIENT_ID, + redirect_uri: REDIRECT_URI, + response_type: "code", + scope: "openid", + code_challenge: challenge, + code_challenge_method: "S256", + }); + const event = makeEvent({ + method: "GET", + url: `${TEST_ISSUER_URL}/oidc/authorize?${params.toString()}`, + locals: { db: mem.db, tenant, user: other, session: otherSession, env: mem.env }, + }); + const { location } = await catchRedirect(() => authorizeGET(event)); + const dest = new URL(location); + expect(dest.searchParams.get("error")).toBe("access_denied"); + expect(dest.searchParams.get("code")).toBeNull(); + }); +}); diff --git a/test/integration/secret-rotation.test.ts b/test/integration/secret-rotation.test.ts new file mode 100644 index 0000000..7f462eb --- /dev/null +++ b/test/integration/secret-rotation.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { ensureDefaultTenant } from "../../src/lib/server/auth/bootstrap"; +import { generateRsaSigningKey, wrapPrivateKey, getActiveSigningKey, invalidateSigningKeyCache, encryptSecret, decryptSecret, tryWithSecrets } from "../../src/lib/server/crypto/keys"; +import { signingKeys } from "../../src/lib/server/db/schema"; +import { openMemoryDb, makePlatform, TEST_SIGNING_SECRET, TEST_SIGNING_SECRET_PREVIOUS, type MemoryDb } from "./harness"; +import type { Tenant } from "../../src/lib/server/db/schema"; + +// Phase 9: 무중단 시크릿 회전. 발급/암호화는 항상 current 만 쓰되, 복호/검증은 current→previous +// 순차 fallback 한다. "previous 로 암호화된 데이터가 회전(current 승격) 후에도 복호되는지" 를 +// 실 DB 서명키 행 + 실제 getActiveSigningKey/tryWithSecrets 경로로 검증한다. + +let mem: MemoryDb; +let tenant: Tenant; + +beforeEach(async () => { + mem = await openMemoryDb(); + tenant = await ensureDefaultTenant(mem.db, makePlatform(mem.env)); +}); + +afterEach(() => { + invalidateSigningKeyCache(tenant.id); + mem.close(); +}); + +describe("Phase 9 — 시크릿 current/previous fallback", () => { + it("previous 로 래핑된 서명키를 current 로만은 복호 못 하고, [current, previous] fallback 으로 복호한다", async () => { + // 회전 이전 시점에 previous(=과거 current)로 private key 를 래핑해 저장했다고 가정. + const { kid, privateKey, publicJwk } = await generateRsaSigningKey(); + const privateJwkEncrypted = await wrapPrivateKey(privateKey, TEST_SIGNING_SECRET_PREVIOUS); + await mem.db.insert(signingKeys).values({ + id: crypto.randomUUID(), + tenantId: tenant.id, + kid, + use: "sig", + alg: "RS256", + publicJwk: JSON.stringify(publicJwk), + privateJwkEncrypted, + active: true, + }); + + // current(신규 시크릿) 단독으로는 unwrap 실패 → tryWithSecrets 가 마지막 에러를 throw. + invalidateSigningKeyCache(tenant.id); + await expect(getActiveSigningKey(mem.db, tenant.id, [TEST_SIGNING_SECRET])).rejects.toThrow(); + + // [current, previous] fallback 으로는 previous 로 성공 복호 → 활성 키 반환. + invalidateSigningKeyCache(tenant.id); + const key = await getActiveSigningKey(mem.db, tenant.id, [TEST_SIGNING_SECRET, TEST_SIGNING_SECRET_PREVIOUS]); + expect(key).not.toBeNull(); + expect(key!.kid).toBe(kid); + // 실제 복호된 privateKey 로 서명이 가능한지(RS256 sign)까지 확인. + const sig = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key!.privateKey, new TextEncoder().encode("payload")); + expect(sig.byteLength).toBeGreaterThan(0); + }); + + it("previous 로 암호화한 일반 시크릿(LDAP bindPassword 형식)이 회전 후 [current, previous] 로 복호된다", async () => { + const plaintext = "ldap-bind-password-super-secret"; + const context = "idp-ldap-bind-password-v1"; + // 회전 전: previous 시크릿으로 암호화된 값이 DB(configJson)에 남아 있는 상태. + const encrypted = await encryptSecret(plaintext, TEST_SIGNING_SECRET_PREVIOUS, context); + + // 회전 후 current 단독 복호는 실패. + await expect(decryptSecret(encrypted, TEST_SIGNING_SECRET, context)).rejects.toThrow(); + + // current→previous fallback(실 로그인 경로와 동일 패턴)은 성공. + const decrypted = await tryWithSecrets([TEST_SIGNING_SECRET, TEST_SIGNING_SECRET_PREVIOUS], (s) => decryptSecret(encrypted, s, context)); + expect(decrypted).toBe(plaintext); + }); + + it("발급/암호화는 current 로만 수행한다 — current 로 래핑한 키는 current 단독으로 복호된다", async () => { + const { kid, privateKey, publicJwk } = await generateRsaSigningKey(); + const privateJwkEncrypted = await wrapPrivateKey(privateKey, TEST_SIGNING_SECRET); + await mem.db.insert(signingKeys).values({ + id: crypto.randomUUID(), + tenantId: tenant.id, + kid, + use: "sig", + alg: "RS256", + publicJwk: JSON.stringify(publicJwk), + privateJwkEncrypted, + active: true, + }); + invalidateSigningKeyCache(tenant.id); + const key = await getActiveSigningKey(mem.db, tenant.id, [TEST_SIGNING_SECRET]); + expect(key!.kid).toBe(kid); + }); +}); diff --git a/test/integration/session-refresh-cascade.test.ts b/test/integration/session-refresh-cascade.test.ts new file mode 100644 index 0000000..43d3365 --- /dev/null +++ b/test/integration/session-refresh-cascade.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { eq } from "drizzle-orm"; +import { actions as sessionActions } from "../../src/routes/account/sessions/+page.server"; +import { issueRefreshToken, rotateRefreshToken } from "../../src/lib/server/oidc/refresh"; +import { oidcRefreshTokens, sessions } from "../../src/lib/server/db/schema"; +import { openMemoryDb, seedTenantAndSigningKey, seedUser, seedSession, makeEvent, TEST_ISSUER_URL, type MemoryDb } from "./harness"; +import type { Tenant, User, Session } from "../../src/lib/server/db/schema"; + +// Phase 6: 개별 세션 철회 → 그 세션에 묶인 OIDC refresh token 연쇄 폐기. +// 실제 account/sessions revoke 액션을 직접 호출해 검증한다. + +let mem: MemoryDb; +let tenant: Tenant; +let user: User; +let sessionA: Session; +let sessionB: Session; + +beforeEach(async () => { + mem = await openMemoryDb(); + tenant = await seedTenantAndSigningKey(mem); + user = await seedUser(mem.db, { tenantId: tenant.id, email: "bob@test.example", username: "bob", password: "pw" }); + sessionA = (await seedSession(mem.db, { tenantId: tenant.id, userId: user.id })).session; + sessionB = (await seedSession(mem.db, { tenantId: tenant.id, userId: user.id })).session; +}); + +afterEach(() => mem.close()); + +async function activeTokenCount(sessionId: string): Promise { + const rows = await mem.db.select().from(oidcRefreshTokens).where(eq(oidcRefreshTokens.sessionId, sessionId)); + return rows.filter((r) => !r.revokedAt).length; +} + +describe("Phase 6 — 세션 개별 철회 시 refresh token 연쇄 폐기", () => { + it("revoke 액션이 대상 세션의 refresh token 만 폐기하고, 폐기된 토큰은 회전이 거부된다", async () => { + // 두 세션 각각에 refresh token 발급 + const tokenA = await issueRefreshToken(mem.db, { tenantId: tenant.id, clientId: "c1", userId: user.id, sessionId: sessionA.id, scope: "openid offline_access" }); + await issueRefreshToken(mem.db, { tenantId: tenant.id, clientId: "c1", userId: user.id, sessionId: sessionB.id, scope: "openid offline_access" }); + + expect(await activeTokenCount(sessionA.id)).toBe(1); + expect(await activeTokenCount(sessionB.id)).toBe(1); + + // sessionA 를 개별 철회 (locals.session = sessionB 이므로 현재 세션이 아님 → JSON 반환) + const event = makeEvent({ + method: "POST", + url: `${TEST_ISSUER_URL}/account/sessions`, + form: { id: sessionA.id }, + locals: { db: mem.db, tenant, user, session: sessionB, env: mem.env }, + }); + const result = await sessionActions.revoke(event); + expect(result).toEqual({ revoked: true }); + + // sessionA 의 refresh token 은 연쇄 폐기, sessionB 는 살아있음 + expect(await activeTokenCount(sessionA.id)).toBe(0); + expect(await activeTokenCount(sessionB.id)).toBe(1); + + // 폐기된 토큰의 회전은 reuse 로 거부된다(연쇄 폐기 실효성 확인) + const rot = await rotateRefreshToken(mem.db, tenant.id, "c1", tokenA); + expect(rot.ok).toBe(false); + if (!rot.ok) expect(rot.reason).toBe("reuse"); + + // 세션 자체도 revoked 로 마킹됨 + const [sa] = await mem.db.select().from(sessions).where(eq(sessions.id, sessionA.id)).limit(1); + expect(sa.revokedAt).not.toBeNull(); + }); + + it("존재하지 않는 세션 id 는 404 로 거부한다(IDOR 방지 가드)", async () => { + const event = makeEvent({ + method: "POST", + url: `${TEST_ISSUER_URL}/account/sessions`, + form: { id: crypto.randomUUID() }, + locals: { db: mem.db, tenant, user, session: sessionB, env: mem.env }, + }); + const result = (await sessionActions.revoke(event)) as { status?: number }; + expect(result.status).toBe(404); + }); +}); diff --git a/test/stubs/app-environment.ts b/test/stubs/app-environment.ts new file mode 100644 index 0000000..fe4d364 --- /dev/null +++ b/test/stubs/app-environment.ts @@ -0,0 +1,16 @@ +// vitest 용 $app/environment 스텁. +// +// 통합 테스트는 실제 서버 모듈(hooks/bootstrap/runtime/route 핸들러)을 직접 import 해 +// 구동하는데, 이들이 SvelteKit 의 `$app/environment` 에서 `dev`/`building` 등을 읽는다. +// 순수 유닛 설정에는 이 alias 가 없어 해석이 깨지므로 최소 스텁을 제공한다. +// +// dev=false 로 둔다 — 이는 "프로덕션 경로" 를 그대로 구동한다는 뜻이다: +// - runtime.resolveIssuerUrl 은 issuerUrl 이 설정돼 있으면 그 값을 반환하고(테스트는 +// platform.env.IDP_ISSUER_URL 로 주입한다), 미설정 시에만 dev 분기를 탄다. +// - bootstrap.assertRequiredConfig 는 ensureAuthBaseline 경로에서만 동작하는데, 하네스는 +// ensureDefaultTenant/ensureSigningKey 를 직접 호출하므로 이 검증 경로를 타지 않는다. +// 따라서 dev=false 라도 통합 테스트는 필요한 값이 주입돼 있어 정상 구동된다. +export const dev = false; +export const building = false; +export const browser = false; +export const version = "test"; diff --git a/vitest.config.ts b/vitest.config.ts index ab8777f..658c9ea 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -17,6 +17,9 @@ export default defineConfig({ "$db-active-driver": resolvePath("./src/lib/server/db/driver-sqlite.ts"), "$env/dynamic/private": resolvePath("./test/stubs/env-dynamic-private.ts"), "$env/static/private": resolvePath("./test/stubs/env-dynamic-private.ts"), + // 통합 테스트(test/integration)가 실 서버 모듈을 직접 구동할 때 필요한 $app/environment 스텁. + // 순수 유닛 테스트는 이 모듈을 import 하지 않으므로 영향이 없다. + "$app/environment": resolvePath("./test/stubs/app-environment.ts"), }, }, test: { From eb6fa9d0fbb7c5125055696baa5d6232dc900770 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 13:47:09 +0900 Subject: [PATCH 15/26] =?UTF-8?q?docs:=20=ED=9B=84=EC=86=8D=202=EC=B0=A8?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0(round2)=20=EA=B3=84=ED=9A=8D=C2=B7?= =?UTF-8?q?=ED=88=AC=EB=91=90=20=EB=AC=B8=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - followup-round2/PLAN.md·TODO.md: P6~P11 페이즈 설계와 페이즈별 실행 투두 기록 Co-Authored-By: Claude Fable 5 --- docs/plans/followup-round2/PLAN.md | 97 ++++++++++++++++ docs/plans/followup-round2/TODO.md | 170 +++++++++++++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 docs/plans/followup-round2/PLAN.md create mode 100644 docs/plans/followup-round2/TODO.md diff --git a/docs/plans/followup-round2/PLAN.md b/docs/plans/followup-round2/PLAN.md new file mode 100644 index 0000000..5b86d15 --- /dev/null +++ b/docs/plans/followup-round2/PLAN.md @@ -0,0 +1,97 @@ +# 후속 2차 개선 기획서 — KeyStone (rate-limit 추상화 제외) + +> 요청: "rate-limit을 제외하고 나머지 계속 다 진행" — 1차 감사(`project-improvement-audit`)에서 후속 후보로 남긴 항목 전부. +> 조사: 2026-07-06, 2개 병렬 Explore. 코드 근거 확인 완료. +> 규칙: 스키마 변경은 `db:generate:all`까지만(적용은 사용자). 커밋은 검증 통과 + 승인 후. + +--- + +## 배경 / 목표 + +1차에서 "별도 트랙" 또는 "후속"으로 남긴 것 중 rate-limit 저장소 추상화만 제외하고 진행: + +- **G1 세션 셀프서비스** — 사용자가 활성 세션 목록을 보고 개별/일괄 철회. (메타 컬럼 이미 존재 → 최저 비용) +- **G2 초대(invite) 플로우** — admin이 초대 메일 발송, 수신자가 비밀번호 최초 설정. +- **G3 계정 삭제(탈퇴) 셀프서비스** — step-up 재인증 후 하드 삭제. +- **G4 마스터 시크릿 다중 버전(무중단 회전)** — old/new secret 동시 시도 + 재암호화 스크립트. +- **G5 잔존 한국어 에러 i18n** — saml-sps/[id], validation.ts, crud-factory/schemas, (정책 결정 후) 프로토콜 API. +- **G6 통합 테스트 하네스** — libSQL 인메모리 + 라우트 직접 호출로 OIDC 풀플로우. + +--- + +## 범위 (In / Out) + +### Phase 6 — 세션 셀프서비스 (G1, 최저 비용) + +- **In**: `listActiveSessions(db, userId)` + `revokeSessionById(db, sessionId, userId)`(IDOR 방지 userId 조건) 신규. 신규 라우트 `account/sessions/`(목록 — ip/userAgent/lastSeenAt/createdAt 표시, `locals.session.id`로 현재 세션 배지, 개별 revoke, "다른 세션 모두 로그아웃"=기존 `revokeOtherSessions` 재사용). 개별 철회 시 `revokeRefreshTokensForSession` 세트 호출(refresh 연쇄 폐기). audit + 보안 알림. i18n ko/en. +- **Out**: step-up 재인증(세션 철회는 passkey 삭제만큼 파괴적이지 않음 — 열린 질문 ②). **스키마 변경 없음.** + +### Phase 7 — 초대 플로우 (G2) + +- **In**: `invite_tokens` 테이블(신규, password_reset 패턴). admin `invite` 액션(비밀번호 없이 계정 생성 — `credentials` row 생략, `emailVerifiedAt` NULL) + 초대 메일. `accept-invite` 라우트(토큰 검증 → 비밀번호 최초 설정 + `emailVerifiedAt` 세팅 → credentials insert, 초대 클릭이 이메일 소유 증명이므로 별도 인증 스킵). 로그인 경로에서 "credential 없는 계정"의 로그인 시도가 안전하게 실패하는지 확인·보강. 계정 생성 원자화(`runAtomic`). i18n. `db:generate:all`. +- **Out**: `users.status`에 `pending` enum 추가(옵션 A) — 대신 **옵션 B 채택**: `emailVerifiedAt IS NULL AND credential 부재`로 초대중 판별(스키마 enum 무변경). admin UI에서 초대중 배지 표시. + +### Phase 8 — 계정 삭제 셀프서비스 (G3) + +- **In**: `account/danger-zone/` 라우트 + self-delete 액션. step-up 재인증(비밀번호 또는 TOTP — passkey delete 패턴 재사용). `assertNotLastAdmin` 재사용(마지막 admin 자기삭제 차단). **하드 삭제**(FK cascade가 credentials/sessions/grants/refresh/saml/memberships 전부 정리; audit는 set null로 보존). audit(`user_self_deleted`) + 탈퇴 확인 메일(세션 만료 전 발송). i18n. +- **Out**: 소프트 삭제/유예기간+복구(enum·GC 배치 필요 — 규모 크고 법적 보존 요건 미정, 열린 질문 ①). 즉시 하드 삭제로 진행. + +### Phase 9 — 마스터 시크릿 무중단 회전 (G4) + +- **In**: `IDP_SIGNING_KEY_SECRET_PREVIOUS` env 추가. `runtime.ts`의 `signingKeySecret: string` → `signingKeySecrets: string[]`(current 우선, previous 후행) 또는 `{current, previous?}`. 공용 `tryWithSecrets(secrets, fn)` 헬퍼로 복호/검증 함수(unwrapPrivateKey/verifyAccessToken/decryptSecret/decryptTotpSecret/HMAC 검증)를 감싸 순차 시도(호출부 최소 변경). **서명·암호화(발급)는 항상 current로**, 검증·복호만 fallback. 재암호화 스크립트 `scripts/reencrypt-secrets.ts`(openScriptDb 패턴 — signing_keys 활성행·TOTP·LDAP bindPassword를 old→new 재작성). `SECRET_ROTATION.md`를 무중단 절차로 갱신. +- **Out**: `audit_events.hash` 재계산 스크립트(선택 후처리 — 별도, 열린 질문 ③). HMAC 도메인 분리 리팩터(#2/#5/#6/#7 원문 공유 — 별도). + +### Phase 10 — 잔존 한국어 에러 i18n (G5) + +- **In**: + - **admin 잔여**: `saml-sps/[id]/+page.server.ts`(7건) `adminError` 전환, `admin/schemas.ts`+`crud-factory.ts` i18n화(`test/unit/admin-zod.test.ts`·`crud-factory.test.ts`의 한국어 기대값 **테스트도 함께 갱신** — 작성/검증 레인 분리 유지). + - **validation.ts**: `reason: string` → `{key, params}`로 시그니처 변경, 호출부(oidc-clients/saml-sps/skins/ldap-providers)에서 `translate()`. + - **프로토콜 API**: 사람이 보는 에러 문구만 i18n(`oidc/saml/webauthn/totp` 네임스페이스 신설). **OAuth/SAML 표준 필드(`error`/`error_description` RFC 규격)는 영어 유지** — RP/SP 파싱 대상이므로. 사용자 대면 HTML 에러 페이지·플래시만 번역. (범위는 열린 질문 ④에서 확정) +- **Out**: `passkey-client/+server.ts`의 클라이언트 JS 문자열(클라이언트 i18n 전략 별도 — 소규모라 이번 포함 검토). + +### Phase 11 — 통합 테스트 하네스 (G6) + +- **In**: `$app/environment` alias 스텁(`dev:false`) 추가로 runtime/bootstrap import 가능화. `drizzle/sqlite/*.sql` 프로그래매틱 적용 유틸(libSQL `:memory:`). 실 라우트 직접 호출 통합 테스트: **OIDC authorize→token→userinfo 풀플로우**(실 DB·실 서명키). 이 하네스 위에 Phase 6~10 신규 로직의 통합 테스트도 추가(초대 수락, 세션 철회, 시크릿 fallback). +- **Out**: SAML SSO 통합(XML 서명 fixture 비용 큼 — 후속), 로그인+MFA HTTP 통합(어댑터-node 기동 필요 — 후속), Playwright 브라우저 e2e(신규 의존성·부트스트랩 — ROI 최하, 제외). + +--- + +## 코드베이스 사실관계 (조사 근거) + +- **세션**: 메타(ip/userAgent/lastSeenAt/createdAt) 이미 저장(`schema.sqlite.ts:180-208`), `locals.session.id`로 현재 세션 식별. `revokeSessionById`·목록 헬퍼만 신규. refresh 연쇄는 `revokeRefreshTokensForSession`(`refresh.ts:73-78`) 세트 호출 필요. +- **초대**: `credentials.secret` nullable(`schema.sqlite.ts:92`) → 비밀번호 없는 계정 생성이 스키마상 가능. `username` nullable, `email` NOT NULL. status enum엔 pending 없음(`:45-47`). admin create는 password 필수(`admin/users/+page.server.ts:77-84`), users+credentials insert가 비원자적(`:109-126`). +- **삭제**: 모든 직접 자식 FK cascade(credentials/sessions/grants/refresh/saml/memberships), audit는 set null. `assertNotLastAdmin`(`guards.ts:40-61`) 재사용. +- **시크릿**: `signingKeySecret?: string` 단일(`runtime.ts:7,32`), 소비처 25+ 파일. 복호/검증 함수 전부 `secret: string` 단일 인자 순수함수 → 래퍼로 순차 시도 가능. `bootstrap.ts:62` unwrapPrivateKey는 try/catch 없음(전수 점검 필요). 재암호화 스크립트 자리 = `scripts/`(openScriptDb 패턴). +- **i18n 잔여**: saml-sps/[id] 7건, validation.ts 6 reason 문자열(시그니처 변경 필요), schemas.ts 8건+crud-factory 2건(테스트가 한국어 단정), 프로토콜 60건+(SAML 31·WebAuthn 19·OIDC 9·TOTP 4). `translate(locale, key)`(`i18n/server.ts:5`), `locals.locale`(`hooks.server.ts:48`). +- **테스트**: vitest 순수 유닛만, 라우트 구동 0건. runtime/bootstrap만 `$app/environment` 사용(alias 부재로 현재 import 불가). libSQL 인메모리 가능(`@libsql/client` 기설치), `scripts/lib/db.ts`에 SQL 실행 유틸 존재. adapter-node 경로가 e2e 마찰 최소. + +--- + +## 접근법 / 리스크 / 순서 + +| Phase | 접근 | 리스크 & 완화 | +| --------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| 6 세션 | 기존 함수·account 라우트 패턴 복제, 스키마 무변경 | 개별 철회 시 refresh 연쇄 누락 → 세트 호출 강제. IDOR → userId 조건 필수 | +| 7 초대 | credentials 생략 계정 + 토큰 패턴, 옵션 B(enum 무변경) | credential 없는 계정 로그인 안전 실패 확인이 관문 → 로그인 경로 점검·보강 필수 | +| 8 삭제 | admin delete 복제 + step-up + assertNotLastAdmin | 비가역 → step-up 재인증·확인 메일·마지막 admin 차단 | +| 9 시크릿 | tryWithSecrets 래퍼(호출부 최소 변경), 발급은 current | 25+ 소비처 파급 → 헬퍼로 국소화, unwrapPrivateKey 무보호 지점 전수 점검. 재암호화 스크립트는 사용자가 실행(적용 안 함) | +| 10 i18n | admin/validation은 완전 전환, 프로토콜은 사용자 대면만(표준 필드 영어 유지) | 테스트 계약 깨짐 → 작성/검증 레인 분리해 테스트 동반 갱신. 표준 준수 위해 범위 신중 | +| 11 테스트 | libSQL 인메모리 + 라우트 직접 호출 | $app/environment 스텁·마이그레이션 적용 유틸이 관문 → Phase 11을 6~10 뒤에 둬 신규 로직까지 커버 | + +**순서**: 6 → 7 → 8(계정 생명주기 묶음, 일부 병렬) → 9(독립) → 10(독립, 병렬 가능) → 11(마지막, 신규 로직 통합 테스트 포함). 6·9·10은 파일 겹침 적어 병렬 착수 가능. + +--- + +## 열린 질문 (승인 시 확정) + +1. **계정 삭제(Phase 8) 방식** — 즉시 하드 삭제(권장, 저비용) vs 소프트 삭제+유예기간(법적 보존/오탈퇴 복구 필요 시). 하드 삭제로 진행할지? +2. **세션/삭제 step-up 재인증** — 세션 개별 철회에도 비밀번호 재확인을 요구할지(계정 삭제는 필수 권장). 세션은 생략, 삭제만 step-up이 기본안. +3. **audit_events.hash 재계산(Phase 9)** — 무중단 회전에 필수는 아님(향후 무결성 검증 오탐 방지용). 이번 스크립트에 포함할지, 제외할지(권장: 제외, 문서로만 안내). +4. **프로토콜 API i18n 범위(Phase 10)** — (a) 사용자 대면 문구만 번역·표준 필드 영어 유지(권장), (b) 전량 번역, (c) 프로토콜은 이번 제외하고 admin/validation만. SAML/WebAuthn가 60건+라 범위가 작업량을 크게 좌우. + +--- + +## 산출물 + +- `docs/plans/followup-round2/PLAN.md` (본 문서) → 승인 후 `TODO.md` +- 스키마 변경분(invite_tokens 등)은 `db:generate:all`까지만 — 적용은 사용자. diff --git a/docs/plans/followup-round2/TODO.md b/docs/plans/followup-round2/TODO.md new file mode 100644 index 0000000..3b49cba --- /dev/null +++ b/docs/plans/followup-round2/TODO.md @@ -0,0 +1,170 @@ +# 실행 투두 — 후속 2차 개선 (6 페이즈) + +> 승인(2026-07-06, 게이트①): 기획 승인 · Phase 8 = **소프트 삭제+유예기간**(enum 확장+GC 배치) · Phase 10 프로토콜 = **전량 번역**(표준 필드 포함) · Phase 9 audit hash 재계산 = **제외**(문서 안내만). +> 규칙: 스키마 변경은 `db:generate:all`까지만(적용 금지, CLAUDE.md). 커밋은 전체 검증 통과 + 커밋 승인 후, 푸시 금지. 쓰기=Opus 위임, 검증=작성자와 분리된 독립 Opus. 스텁/TODO/skip은 블로커. +> 순서: 6·9·10 병렬 착수 가능 → 7 → 8(7의 계정 생명주기 확장) → 11(마지막, 신규 로직 통합 테스트 포함). 스키마 건드리는 7·8은 마이그레이션 생성 순서 조율(직렬). + +--- + +## Phase 6 — 세션 셀프서비스 (스키마 무변경, 최저 비용) + +**목적**: 사용자가 활성 세션 목록 확인 + 개별/일괄 철회. + +### [x] 6-1. 세션 조회·철회 헬퍼 + +- 파일: `src/lib/server/auth/session.ts` +- 작업: `listActiveSessions(db, userId)`(revokedAt null·미만료 세션의 id/ip/userAgent/lastSeenAt/createdAt 반환), `revokeSessionById(db, sessionId, userId, revokedAt)`(**userId 조건 필수** — IDOR 방지, sessionId+userId 동시 일치만 revoke). +- 수용 기준: 타 사용자 sessionId 철회 불가(userId 불일치 시 no-op/0행). 만료·이미 revoked 세션은 목록 제외. + +### [x] 6-2. account/sessions 라우트 + +- 파일: `src/routes/account/sessions/+page.server.ts`·`+page.svelte`(신규) +- 작업: load(목록 + `locals.session.id`로 "현재 세션" 배지, account 라우트 관례 준수). 액션 `revoke`(개별 — `revokeSessionById` + **`revokeRefreshTokensForSession` 세트 호출**로 refresh 연쇄 폐기), `revokeOthers`(기존 `revokeOtherSessions` 재사용, 현재 세션 유지). audit(`session_revoked`) + `dispatchSecurityAlert`(best-effort). i18n ko/en. 현재 세션 개별 철회 시 로그아웃 처리 일관성 확인. +- 수용 기준: 목록에 메타 정상 표시, 개별 철회 시 해당 세션+refresh 폐기, 다른 세션 유지. IDOR 없음. ko/en 대칭. + +### [x] 6-검증 (독립 Opus) — 통과(2026-07-06, APPROVE): IDOR·refresh 연쇄·현재세션 검증. 보안알림은 스코프 제약으로 audit 갈음(후속 소량 추가 여지) + +- IDOR(userId 조건), refresh 연쇄 폐기 세트 호출, 현재 세션 철회 동작. 게이트 4종. + +--- + +## Phase 7 — 초대 플로우 (스키마 변경 — 옵션 B: enum 무변경) + +**목적**: admin 초대 메일 → 수신자 비밀번호 최초 설정. "초대중" = `emailVerifiedAt IS NULL AND credential 부재`. + +### [x] 7-0. 로그인 안전성 선점검 (관문) + +- 파일: `src/lib/server/auth/users.ts`(로그인 크레덴셜 조회), `src/routes/(auth)/login/+page.server.ts` +- 작업: password credential이 **없는** 계정의 로그인 시도가 안전하게 실패하는지 확인. 취약(예외/우회)하면 보강 — credential 부재 시 명확한 인증 실패(타이밍 균등화 유지, Phase 1의 더미 scrypt 경로와 정합). LDAP/패스키 사용자와의 구분 확인. +- 수용 기준: credential 없는(초대 대기) 계정으로 비밀번호 로그인 불가. 사용자 열거 오라클 신규 발생 없음. + +### [x] 7-1. invite_tokens 테이블 + +- 파일: `src/lib/server/db/schema.{sqlite,pg,mysql}.ts` +- 작업: `password_reset_tokens` 동일 구조(userId cascade FK, tokenHash, expiresAt, usedAt, createdAt) 3방언 추가. TTL은 발급부에서(예: 72시간). parity 유지. +- 수용 기준: 3방언 컬럼·인덱스 동일. schema-parity 테스트 통과. + +### [x] 7-2. 발급·메일·admin invite 액션 + +- 파일: `src/lib/server/auth/invite.ts`(신규, email-verification 패턴), `src/lib/server/email.ts`(`sendInviteEmail`), `src/routes/admin/users/+page.server.ts`(`invite` 액션) +- 작업: admin `invite` 액션 — email/displayName/role 받아 **비밀번호 없이** 계정 생성(credentials row 생략, emailVerifiedAt NULL), 초대 토큰 발급+메일. users insert를 `runAtomic`로(기존 create의 비원자성 답습 금지). 기존 create 액션은 유지(직접 비밀번호 생성 경로 병존). +- 수용 기준: 초대 계정 생성 + 메일 발송(best-effort 격리). role 검증. 중복 이메일 차단. audit(`user_invited`). + +### [x] 7-3. accept-invite 라우트 + +- 파일: `src/routes/(auth)/accept-invite/+page.server.ts`·`+page.svelte`(신규, verify-email/reset-password 패턴) +- 작업: 토큰 검증(해시·만료·1회용) → 비밀번호 최초 설정(정책 준수) + `emailVerifiedAt` 세팅(초대 클릭=이메일 소유 증명) + credentials(password) insert를 원자적으로(`runAtomic`). i18n. `db:generate:all`(7-1 반영). +- 수용 기준: 유효 토큰으로 비밀번호 설정→로그인 가능. 만료/재사용 거부. emailVerifiedAt·credential 동시 반영(원자). admin UI에 초대중 배지. + +### [x] 7-검증 (독립 Opus) — 통과(2026-07-06, APPROVE): 열거 오라클 없음, 원자성·토큰 보안·정상계정 구분 확인. federated 사용자 배지 오표시(V5 엣지) 별도 수정 + +- 로그인 선점검 보강 유효성, 토큰 보안(해시·1회용·만료), 계정 생성/수락 원자성, 초대중 판별. 마이그레이션 생성만. 게이트. + +--- + +## Phase 8 — 계정 삭제 셀프서비스 (소프트 삭제 + 유예기간) + +**목적**: step-up 재인증 후 탈퇴 신청 → 유예기간 후 GC 하드 삭제. 복구 가능. + +### [x] 8-1. status enum 확장 + 삭제예정 메타 + +- 파일: `src/lib/server/db/schema.{sqlite,pg,mysql}.ts` +- 작업: `users.status` enum에 `deletion_pending`(또는 유사) 추가(3방언 동시). 하드삭제 시점 판단용 `deletionScheduledAt`(nullable timestamp_ms) 컬럼 추가. parity 유지. `db:generate:all`. +- 수용 기준: 3방언 enum·컬럼 동일. 기존 status 값 무영향. parity 통과. + +### [x] 8-2. self-delete(탈퇴 신청) 액션 + +- 파일: `src/routes/account/danger-zone/+page.server.ts`·`+page.svelte`(신규) +- 작업: step-up 재인증(비밀번호 또는 TOTP — passkey delete 패턴 재사용). `assertNotLastAdmin`(마지막 admin 자기삭제 차단). 상태를 `deletion_pending` + `deletionScheduledAt = now + 유예(예: 30일)` 세팅, 전 세션+refresh 즉시 폐기(로그아웃). audit(`user_deletion_requested`) + 탈퇴 접수 메일(세션 만료 전 발송). 복구 안내 포함. +- 수용 기준: step-up 없이는 탈퇴 불가. 마지막 admin 차단. 신청 즉시 로그아웃+상태 전환, 유예기간 기록. 확인 메일 발송. + +### [x] 8-3. 로그인 차단 + 복구 + GC 하드삭제 + +- 파일: `src/routes/(auth)/login/+page.server.ts`(deletion_pending 처리), 복구 경로(로그인 시 유예 내 복구 옵션 또는 admin 복구), `src/lib/server/db/gc.ts`(유예 경과분 하드 삭제) +- 작업: `deletion_pending` 계정 로그인 시 → 유예기간 내면 "복구하시겠습니까"(복구=status active 환원, deletionScheduledAt null) 또는 로그인 거부. GC에 `deletionScheduledAt < now`인 users 하드 삭제 추가(FK cascade가 자식 정리, audit set null 보존). 보수적 삭제 조건. +- 수용 기준: 유예 내 복구 가능. 유예 경과 시 GC가 하드 삭제(미경과·활성 계정 절대 삭제 안 함). 로그인 흐름 일관. + +### [x] 8-검증 (독립 Opus) — 통과(2026-07-06, APPROVE): GC 활성/미경과 계정 삭제 불가(NULL 비교), step-up 우회 불가, 타이밍 오라클 없음, 복구 회귀 없음, 배지 정확(prettier 2건 정리). 엣지: 스킨 로그인은 복구 패널 미표시(기본 스킨만, 후속) + +- step-up 우회 불가, 마지막 admin 차단, GC 삭제 조건 보수성(미경과 삭제 없음), 복구 경로, cascade 정합. 마이그레이션 생성만. 게이트. + +--- + +## Phase 9 — 마스터 시크릿 무중단 회전 (audit 재계산 제외) + +**목적**: old/new secret 동시 검증 + 재암호화 스크립트로 무중단 회전. + +### [x] 9-1. runtime 다중 시크릿 + tryWithSecrets 헬퍼 + +- 파일: `src/lib/server/auth/runtime.ts`, `src/lib/server/crypto/keys.ts`(헬퍼 배치) +- 작업: `IDP_SIGNING_KEY_SECRET_PREVIOUS` env 추가. `signingKeySecret: string` → `signingKeySecrets: string[]`(current 우선, previous 후행; previous 없으면 길이1). 공용 `tryWithSecrets(secrets, fn)`(순차 시도, 마지막 실패만 throw) 도입. **발급/암호화(wrapPrivateKey/서명/encryptSecret)는 항상 `secrets[0]`(current)**, 검증/복호만 fallback. +- 수용 기준: previous 미설정 시 기존과 동일 동작(회귀 0). current로 실패한 복호가 previous로 성공. 발급은 current 고정. + +### [x] 9-2. 복호/검증 소비처 fallback 적용 + +- 파일: unwrapPrivateKey/verifyAccessToken/decryptSecret/decryptTotpSecret 호출부 및 HMAC 검증부(mfa/webauthn/audit 검증), `bootstrap.ts:62`(try/catch 없는 unwrapPrivateKey 전수 점검) +- 작업: 검증·복호 경로를 `tryWithSecrets`로 감싸 current→previous 순차. **소비처 25+ 파일 전수 점검** — 발급 경로가 실수로 previous를 쓰지 않는지, 무보호 예외 지점(bootstrap unwrap)이 fallback 루프에 들어가는지. RuntimeConfig 타입 변경 파급을 기계적으로 반영. +- 수용 기준: 모든 복호/검증이 회전 창에서 old/new 양쪽 수용. 발급은 current. 회귀 0(전체 게이트+통합 테스트). + +### [x] 9-3. 재암호화 스크립트 + 문서 + +- 파일: `scripts/reencrypt-secrets.ts`(신규, openScriptDb 패턴), `docs/SECRET_ROTATION.md`(무중단 절차로 갱신) +- 작업: signing_keys 활성행·credentials(totp)·identity_providers(LDAP bindPassword)를 old→new 재작성(4방언, `DB_DIALECT` 분기). dry-run 옵션 권장. **스크립트는 사용자가 실행**(자동 실행 금지). SECRET_ROTATION.md를 "previous 설정→배포→재암호화→previous 제거" 무중단 순서로 갱신, audit hash 재계산은 **제외하되 필요 시 수동 절차만 문서 안내**. +- 수용 기준: 스크립트가 dry-run으로 대상 건수 보고. 실제 재작성 로직이 unwrap(any secret)→wrap(current) 정확. 문서가 무중단 절차 반영. 자동 실행 안 함. + +### [x] 9-검증 (독립 Opus) — 통과(2026-07-06, APPROVE): 발급=current 위반 0건, fallback 헬퍼 선택 정확, 무보호 지점 커버, reencrypt dry-run. 158 테스트 + +- 발급=current 고정(previous 오사용 없음), 복호 fallback 정확, 무보호 예외 지점 커버, 재암호화 로직 정합. 게이트+통합 테스트. + +--- + +## Phase 10 — 잔존 한국어 에러 i18n (프로토콜 전량 번역) + +**목적**: admin 잔여 + validation.ts + 프로토콜 API(60건+) 전량 i18n. ko/en 대칭. + +### [x] 10-1. admin 잔여 + validation.ts + +- 파일: `src/routes/admin/saml-sps/[id]/+page.server.ts`(7건 `adminError`), `src/lib/server/admin/schemas.ts`+`crud-factory.ts`(i18n화), `src/lib/server/validation.ts`(`reason: string`→`{key,params}` 시그니처 변경)+호출부(oidc-clients/saml-sps/skins/ldap-providers), `src/lib/i18n/{ko,en}.json` +- 작업: schemas/crud-factory i18n화 시 **`test/unit/admin-zod.test.ts`·`crud-factory.test.ts`의 한국어 기대값도 함께 갱신**(별도 검증 레인은 유지 — 작성자가 테스트 갱신, 독립 에이전트가 검증). validation reason은 키+파라미터로 바꿔 호출부에서 `translate(locals.locale, ...)`. +- 수용 기준: 대상 파일 하드코딩 한국어 0(grep). 갱신된 테스트 통과. ko/en 대칭. 에러 shape 무변경. + +### [x] 10-2. 프로토콜 API 전량 번역 + +- 파일: OIDC(`oidc/{authorize,introspect,revoke,end-session,userinfo}`), SAML(`saml/{sso,slo,metadata}`), WebAuthn(`api/webauthn/**` + `passkey-client/+server.ts` 클라이언트 문자열), TOTP(`api/totp/{verify,enroll/confirm}`), `src/lib/i18n/{ko,en}.json`(oidc/saml/webauthn/totp 네임스페이스 신설) +- 작업: 사람이 보는 에러 문구를 `translate(locals.locale, ".errors.")`로 전환. **전량 번역 결정** — 단, OAuth `error`/SAML StatusCode 등 **기계 파싱 규격 코드값 자체는 표준 유지**하고 `error_description`·사람 대면 텍스트를 번역(표준 필드에 로케일 문구를 넣는 것이 규격 위반이 아닌 범위에서). `passkey-client`의 클라이언트 JS 문자열은 서버에서 로케일별 문자열을 주입하는 방식으로. +- 수용 기준: 프로토콜 라우트 사용자 대면 한국어 리터럴 0(grep). RP/SP 파싱 대상 코드값은 불변(표준 준수 확인). ko/en 대칭. 각 라우트 정상 동작. + +### [x] 10-검증 (독립 Opus) — 통과(2026-07-06, APPROVE): 표준 error 코드·StatusCode 보존, 사람대면 한국어 0, 704 대칭, 82키 신설. token 제외 타당. + 인접 버그(mfa_login 5키 부재) 별도 수정 + +- 한국어 리터럴 grep 0, 표준 코드값 불변(OAuth/SAML 파싱 호환), ko/en 대칭, 테스트 갱신 정합. 게이트. + +--- + +## Phase 11 — 통합 테스트 하네스 (마지막) + +**목적**: 실 DB(libSQL 인메모리) + 실 라우트 직접 호출로 OIDC 풀플로우 + 6~10 신규 로직 통합 검증. + +### [x] 11-1. 하네스 구축 + +- 파일: `vitest.config.ts`(`$app/environment` alias 스텁 추가), `test/integration/harness.ts`(신규 — libSQL `:memory:` + `drizzle/sqlite/*.sql` 프로그래매틱 적용, RequestEvent 빌더) +- 작업: `$app/environment` 스텁(`dev:false` export)로 runtime/bootstrap import 가능화. `scripts/lib/db.ts`의 SQL 실행 유틸 참고해 마이그레이션 순차 적용. `IDP_SIGNING_KEY_SECRET`/`IDP_ISSUER_URL` 테스트 값 주입. 테스트 격리(케이스별 새 DB 또는 트랜잭션 롤백). +- 수용 기준: 하네스로 실 스키마 인메모리 DB 구성 + 라우트 핸들러 import 성공. 순수 유닛 테스트(기존 151)와 공존(설정 충돌 없음). + +### [x] 11-2. 통합 테스트 작성 + +- 파일: `test/integration/*.test.ts`(신규) +- 작업: **OIDC authorize→token→userinfo 풀플로우**(실 서명키·PKCE·code 소진·클레임). 6~10 신규 로직: 세션 개별 철회(refresh 연쇄), 초대 수락(credential 생성), 계정 삭제 유예/복구, 시크릿 current/previous fallback(회전 시나리오). 프로토콜 i18n 로케일별 에러 문구 스모크. +- 수용 기준: 풀플로우 그린. 신규 로직 통합 경로 커버. `test.skip`/tautology 없음. 전체 `bun run test` 그린. + +### [x] 11-검증 (독립 Opus) — 통과(2026-07-06, APPROVE): 실 libSQL DB+실 라우트 구동(tautology 없음), 159 유닛 + 15 통합 = 174 그린 + +- 하네스가 실 SQL/실 라우트 구동하는지(mock-echo 아님), 격리 정상, 풀플로우 단정 실질성. 게이트 전체. + +--- + +## 최종 게이트 & 커밋 (STEP 7-8) + +- [ ] 전체: `bun run test`(통합 포함) · `check` · `lint` · `build` · `typecheck` · `db:check`(4방언) · `db:generate:all` no-op — 전부 그린 +- [ ] 마이그레이션: invite_tokens·users status enum/deletionScheduledAt 생성물 보고, **적용은 사용자 요청** (migrate/push 금지) +- [ ] 재암호화 스크립트: 실행하지 않음(사용자 몫), dry-run 사용법 안내 +- [ ] 커밋 승인 게이트 → 페이즈별 원자 커밋(리포 스타일). 푸시 금지. From e1da8784acf50a1bef4258765e55c716a76446e4 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 17:38:07 +0900 Subject: [PATCH 16/26] =?UTF-8?q?chore(db):=203=EC=B0=A8=20=EB=A7=88?= =?UTF-8?q?=EA=B0=90=20=EC=8A=A4=ED=82=A4=EB=A7=88=203=ED=8A=B8=EB=9E=99?= =?UTF-8?q?=20=E2=80=94=20users=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=C2=B7?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=EB=B3=80=EA=B2=BD=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=C2=B7organization=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - users: deletion_pending 하드삭제 배치 조회용 인덱스 추가(방언별 부분/일반 인덱스) — d1 0025 / pg·mysql·sqlite 0008 - pendingEmail·pendingEmailRequestedAt 컬럼 + email_change_tokens 테이블 신설(SHA-256·1회용·만료, 이메일 변경 플로우 기반) — d1 0026 / *0009 - oidcClients.organizationClaimConfig(JSON text) 컬럼 — organization 클레임 클라이언트별 노출 토글 저장(null=전량 노출 하위호환) — d1 0027 / *0010 - P12(users 인덱스)·P13(이메일 변경)·P17(organization config) 세 페이즈가 공유하는 스키마 기반이라 한 커밋으로 묶음(journal/snapshot 얽힘 회피). 적용은 별도(사용자). Co-Authored-By: Claude Fable 5 --- drizzle/0025_organic_landau.sql | 1 + drizzle/0026_sad_doctor_faustus.sql | 15 + drizzle/0027_dizzy_randall_flagg.sql | 1 + drizzle/meta/0025_snapshot.json | 3958 ++++++++++++++ drizzle/meta/0026_snapshot.json | 4061 +++++++++++++++ drizzle/meta/0027_snapshot.json | 4068 +++++++++++++++ drizzle/meta/_journal.json | 21 + drizzle/mysql/0008_slow_malcolm_colcord.sql | 1 + drizzle/mysql/0009_previous_sasquatch.sql | 16 + .../mysql/0010_flimsy_phantom_reporter.sql | 1 + drizzle/mysql/meta/0008_snapshot.json | 4153 +++++++++++++++ drizzle/mysql/meta/0009_snapshot.json | 4263 +++++++++++++++ drizzle/mysql/meta/0010_snapshot.json | 4270 +++++++++++++++ drizzle/mysql/meta/_journal.json | 21 + drizzle/pg/0008_spooky_mindworm.sql | 1 + drizzle/pg/0009_sad_lockjaw.sql | 15 + drizzle/pg/0010_cooing_invaders.sql | 1 + drizzle/pg/meta/0008_snapshot.json | 4447 ++++++++++++++++ drizzle/pg/meta/0009_snapshot.json | 4560 ++++++++++++++++ drizzle/pg/meta/0010_snapshot.json | 4566 +++++++++++++++++ drizzle/pg/meta/_journal.json | 21 + drizzle/sqlite/0008_free_spyke.sql | 1 + drizzle/sqlite/0009_slim_the_order.sql | 15 + drizzle/sqlite/0010_silent_the_call.sql | 1 + drizzle/sqlite/meta/0008_snapshot.json | 3958 ++++++++++++++ drizzle/sqlite/meta/0009_snapshot.json | 4061 +++++++++++++++ drizzle/sqlite/meta/0010_snapshot.json | 4068 +++++++++++++++ drizzle/sqlite/meta/_journal.json | 21 + src/lib/server/db/schema.mysql.ts | 45 +- src/lib/server/db/schema.pg.ts | 44 +- src/lib/server/db/schema.sqlite.ts | 46 +- 31 files changed, 50718 insertions(+), 3 deletions(-) create mode 100644 drizzle/0025_organic_landau.sql create mode 100644 drizzle/0026_sad_doctor_faustus.sql create mode 100644 drizzle/0027_dizzy_randall_flagg.sql create mode 100644 drizzle/meta/0025_snapshot.json create mode 100644 drizzle/meta/0026_snapshot.json create mode 100644 drizzle/meta/0027_snapshot.json create mode 100644 drizzle/mysql/0008_slow_malcolm_colcord.sql create mode 100644 drizzle/mysql/0009_previous_sasquatch.sql create mode 100644 drizzle/mysql/0010_flimsy_phantom_reporter.sql create mode 100644 drizzle/mysql/meta/0008_snapshot.json create mode 100644 drizzle/mysql/meta/0009_snapshot.json create mode 100644 drizzle/mysql/meta/0010_snapshot.json create mode 100644 drizzle/pg/0008_spooky_mindworm.sql create mode 100644 drizzle/pg/0009_sad_lockjaw.sql create mode 100644 drizzle/pg/0010_cooing_invaders.sql create mode 100644 drizzle/pg/meta/0008_snapshot.json create mode 100644 drizzle/pg/meta/0009_snapshot.json create mode 100644 drizzle/pg/meta/0010_snapshot.json create mode 100644 drizzle/sqlite/0008_free_spyke.sql create mode 100644 drizzle/sqlite/0009_slim_the_order.sql create mode 100644 drizzle/sqlite/0010_silent_the_call.sql create mode 100644 drizzle/sqlite/meta/0008_snapshot.json create mode 100644 drizzle/sqlite/meta/0009_snapshot.json create mode 100644 drizzle/sqlite/meta/0010_snapshot.json diff --git a/drizzle/0025_organic_landau.sql b/drizzle/0025_organic_landau.sql new file mode 100644 index 0000000..95101b3 --- /dev/null +++ b/drizzle/0025_organic_landau.sql @@ -0,0 +1 @@ +CREATE INDEX `users_deletion_pending_idx` ON `users` (`deletion_scheduled_at`) WHERE status = 'deletion_pending'; \ No newline at end of file diff --git a/drizzle/0026_sad_doctor_faustus.sql b/drizzle/0026_sad_doctor_faustus.sql new file mode 100644 index 0000000..2c8cc65 --- /dev/null +++ b/drizzle/0026_sad_doctor_faustus.sql @@ -0,0 +1,15 @@ +CREATE TABLE `email_change_tokens` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `token_hash` text NOT NULL, + `target_email` text NOT NULL, + `expires_at` integer NOT NULL, + `used_at` integer, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `email_change_tokens_user_idx` ON `email_change_tokens` (`user_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `email_change_tokens_hash_uidx` ON `email_change_tokens` (`token_hash`);--> statement-breakpoint +ALTER TABLE `users` ADD `pending_email` text;--> statement-breakpoint +ALTER TABLE `users` ADD `pending_email_requested_at` integer; \ No newline at end of file diff --git a/drizzle/0027_dizzy_randall_flagg.sql b/drizzle/0027_dizzy_randall_flagg.sql new file mode 100644 index 0000000..3556bf7 --- /dev/null +++ b/drizzle/0027_dizzy_randall_flagg.sql @@ -0,0 +1 @@ +ALTER TABLE `oidc_clients` ADD `organization_claim_config` text; \ No newline at end of file diff --git a/drizzle/meta/0025_snapshot.json b/drizzle/meta/0025_snapshot.json new file mode 100644 index 0000000..41350d0 --- /dev/null +++ b/drizzle/meta/0025_snapshot.json @@ -0,0 +1,3958 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6f69ef2c-4175-4b32-b599-c3620f350917", + "prevId": "7a8bbdcb-7598-4a45-96a3-8c8bdf6cd1b7", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_verification_tokens": { + "name": "email_verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_login_at": { + "name": "last_login_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite_tokens": { + "name": "invite_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "invite_tokens_user_idx": { + "name": "invite_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "invite_tokens_hash_uidx": { + "name": "invite_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invite_tokens_user_id_users_id_fk": { + "name": "invite_tokens_user_id_users_id_fk", + "tableFrom": "invite_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "rotated_at": { + "name": "rotated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + "tenant_id" + ], + "isUnique": true, + "where": "active = 1" + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "deletion_scheduled_at": { + "name": "deletion_scheduled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "users_deletion_pending_idx": { + "name": "users_deletion_pending_idx", + "columns": [ + "deletion_scheduled_at" + ], + "isUnique": false, + "where": "status = 'deletion_pending'" + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0026_snapshot.json b/drizzle/meta/0026_snapshot.json new file mode 100644 index 0000000..60623d5 --- /dev/null +++ b/drizzle/meta/0026_snapshot.json @@ -0,0 +1,4061 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "79eb5b4d-0076-4a07-8a82-bfbc1a9bf629", + "prevId": "6f69ef2c-4175-4b32-b599-c3620f350917", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_change_tokens": { + "name": "email_change_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_email": { + "name": "target_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "email_change_tokens_user_idx": { + "name": "email_change_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_change_tokens_hash_uidx": { + "name": "email_change_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_change_tokens_user_id_users_id_fk": { + "name": "email_change_tokens_user_id_users_id_fk", + "tableFrom": "email_change_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_verification_tokens": { + "name": "email_verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_login_at": { + "name": "last_login_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite_tokens": { + "name": "invite_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "invite_tokens_user_idx": { + "name": "invite_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "invite_tokens_hash_uidx": { + "name": "invite_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invite_tokens_user_id_users_id_fk": { + "name": "invite_tokens_user_id_users_id_fk", + "tableFrom": "invite_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "rotated_at": { + "name": "rotated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + "tenant_id" + ], + "isUnique": true, + "where": "active = 1" + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_email": { + "name": "pending_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_email_requested_at": { + "name": "pending_email_requested_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "deletion_scheduled_at": { + "name": "deletion_scheduled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "users_deletion_pending_idx": { + "name": "users_deletion_pending_idx", + "columns": [ + "deletion_scheduled_at" + ], + "isUnique": false, + "where": "status = 'deletion_pending'" + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0027_snapshot.json b/drizzle/meta/0027_snapshot.json new file mode 100644 index 0000000..3cffdc4 --- /dev/null +++ b/drizzle/meta/0027_snapshot.json @@ -0,0 +1,4068 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "31218830-88ec-45c4-afba-5e1b12132039", + "prevId": "79eb5b4d-0076-4a07-8a82-bfbc1a9bf629", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_change_tokens": { + "name": "email_change_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_email": { + "name": "target_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "email_change_tokens_user_idx": { + "name": "email_change_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_change_tokens_hash_uidx": { + "name": "email_change_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_change_tokens_user_id_users_id_fk": { + "name": "email_change_tokens_user_id_users_id_fk", + "tableFrom": "email_change_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_verification_tokens": { + "name": "email_verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_login_at": { + "name": "last_login_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite_tokens": { + "name": "invite_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "invite_tokens_user_idx": { + "name": "invite_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "invite_tokens_hash_uidx": { + "name": "invite_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invite_tokens_user_id_users_id_fk": { + "name": "invite_tokens_user_id_users_id_fk", + "tableFrom": "invite_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "organization_claim_config": { + "name": "organization_claim_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "rotated_at": { + "name": "rotated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + "tenant_id" + ], + "isUnique": true, + "where": "active = 1" + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_email": { + "name": "pending_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_email_requested_at": { + "name": "pending_email_requested_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "deletion_scheduled_at": { + "name": "deletion_scheduled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "users_deletion_pending_idx": { + "name": "users_deletion_pending_idx", + "columns": [ + "deletion_scheduled_at" + ], + "isUnique": false, + "where": "status = 'deletion_pending'" + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index d8de696..949ed30 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -176,6 +176,27 @@ "when": 1783310296391, "tag": "0024_robust_arclight", "breakpoints": true + }, + { + "idx": 25, + "version": "6", + "when": 1783319176020, + "tag": "0025_organic_landau", + "breakpoints": true + }, + { + "idx": 26, + "version": "6", + "when": 1783320561755, + "tag": "0026_sad_doctor_faustus", + "breakpoints": true + }, + { + "idx": 27, + "version": "6", + "when": 1783323957251, + "tag": "0027_dizzy_randall_flagg", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/mysql/0008_slow_malcolm_colcord.sql b/drizzle/mysql/0008_slow_malcolm_colcord.sql new file mode 100644 index 0000000..27f02db --- /dev/null +++ b/drizzle/mysql/0008_slow_malcolm_colcord.sql @@ -0,0 +1 @@ +CREATE INDEX `users_deletion_gc_idx` ON `users` (`status`,`deletion_scheduled_at`); \ No newline at end of file diff --git a/drizzle/mysql/0009_previous_sasquatch.sql b/drizzle/mysql/0009_previous_sasquatch.sql new file mode 100644 index 0000000..7b02b98 --- /dev/null +++ b/drizzle/mysql/0009_previous_sasquatch.sql @@ -0,0 +1,16 @@ +CREATE TABLE `email_change_tokens` ( + `id` varchar(64) NOT NULL, + `user_id` varchar(64) NOT NULL, + `token_hash` varchar(255) NOT NULL, + `target_email` varchar(320) NOT NULL, + `expires_at` datetime(3) NOT NULL, + `used_at` datetime(3), + `created_at` datetime(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP(3)), + CONSTRAINT `email_change_tokens_id` PRIMARY KEY(`id`), + CONSTRAINT `email_change_tokens_hash_uidx` UNIQUE(`token_hash`) +); +--> statement-breakpoint +ALTER TABLE `users` ADD `pending_email` varchar(320);--> statement-breakpoint +ALTER TABLE `users` ADD `pending_email_requested_at` datetime(3);--> statement-breakpoint +ALTER TABLE `email_change_tokens` ADD CONSTRAINT `email_change_tokens_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX `email_change_tokens_user_idx` ON `email_change_tokens` (`user_id`); \ No newline at end of file diff --git a/drizzle/mysql/0010_flimsy_phantom_reporter.sql b/drizzle/mysql/0010_flimsy_phantom_reporter.sql new file mode 100644 index 0000000..3556bf7 --- /dev/null +++ b/drizzle/mysql/0010_flimsy_phantom_reporter.sql @@ -0,0 +1 @@ +ALTER TABLE `oidc_clients` ADD `organization_claim_config` text; \ No newline at end of file diff --git a/drizzle/mysql/meta/0008_snapshot.json b/drizzle/mysql/meta/0008_snapshot.json new file mode 100644 index 0000000..5569f66 --- /dev/null +++ b/drizzle/mysql/meta/0008_snapshot.json @@ -0,0 +1,4153 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "4efafe11-11a6-475b-88a8-ad99ed3c7cc7", + "prevId": "ee2036b8-a0ca-404d-b48a-38157a67164d", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_events_id": { + "name": "audit_events_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "client_skins_id": { + "name": "client_skins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credentials_id": { + "name": "credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "departments_id": { + "name": "departments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "email_verification_tokens": { + "name": "email_verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "email_verification_tokens_id": { + "name": "email_verification_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "last_login_at": { + "name": "last_login_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "identities_id": { + "name": "identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "identity_providers_id": { + "name": "identity_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "invite_tokens": { + "name": "invite_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "invite_tokens_user_idx": { + "name": "invite_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "invite_tokens_hash_uidx": { + "name": "invite_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invite_tokens_user_id_users_id_fk": { + "name": "invite_tokens_user_id_users_id_fk", + "tableFrom": "invite_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "invite_tokens_id": { + "name": "invite_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('openid')" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('authorization_code,refresh_token')" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('code')" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('RS256')" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_clients_id": { + "name": "oidc_clients_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_grants_id": { + "name": "oidc_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_refresh_tokens_id": { + "name": "oidc_refresh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "parts_id": { + "name": "parts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "password_reset_tokens_id": { + "name": "password_reset_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "positions_id": { + "name": "positions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rate_limits_key": { + "name": "rate_limits_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_sessions_id": { + "name": "saml_sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_slo_states_id": { + "name": "saml_slo_states_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_sps_id": { + "name": "saml_sps_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_roles_id": { + "name": "service_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "rotated_at": { + "name": "rotated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "signing_keys_id": { + "name": "signing_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "teams_id": { + "name": "teams_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tenants_id": { + "name": "tenants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_departments_id": { + "name": "user_departments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_parts_id": { + "name": "user_parts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_service_assignments_id": { + "name": "user_service_assignments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_teams_id": { + "name": "user_teams_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "deletion_scheduled_at": { + "name": "deletion_scheduled_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('ko-KR')" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('Asia/Seoul')" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "users_deletion_gc_idx": { + "name": "users_deletion_gc_idx", + "columns": [ + "status", + "deletion_scheduled_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_challenges_id": { + "name": "webauthn_challenges_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0009_snapshot.json b/drizzle/mysql/meta/0009_snapshot.json new file mode 100644 index 0000000..7b48b08 --- /dev/null +++ b/drizzle/mysql/meta/0009_snapshot.json @@ -0,0 +1,4263 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "3621640b-14d8-49c0-8c37-b489e3a48df7", + "prevId": "4efafe11-11a6-475b-88a8-ad99ed3c7cc7", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_events_id": { + "name": "audit_events_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "client_skins_id": { + "name": "client_skins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credentials_id": { + "name": "credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "departments_id": { + "name": "departments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "email_change_tokens": { + "name": "email_change_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_email": { + "name": "target_email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "email_change_tokens_user_idx": { + "name": "email_change_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_change_tokens_hash_uidx": { + "name": "email_change_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_change_tokens_user_id_users_id_fk": { + "name": "email_change_tokens_user_id_users_id_fk", + "tableFrom": "email_change_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "email_change_tokens_id": { + "name": "email_change_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "email_verification_tokens": { + "name": "email_verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "email_verification_tokens_id": { + "name": "email_verification_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "last_login_at": { + "name": "last_login_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "identities_id": { + "name": "identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "identity_providers_id": { + "name": "identity_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "invite_tokens": { + "name": "invite_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "invite_tokens_user_idx": { + "name": "invite_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "invite_tokens_hash_uidx": { + "name": "invite_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invite_tokens_user_id_users_id_fk": { + "name": "invite_tokens_user_id_users_id_fk", + "tableFrom": "invite_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "invite_tokens_id": { + "name": "invite_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('openid')" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('authorization_code,refresh_token')" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('code')" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('RS256')" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_clients_id": { + "name": "oidc_clients_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_grants_id": { + "name": "oidc_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_refresh_tokens_id": { + "name": "oidc_refresh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "parts_id": { + "name": "parts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "password_reset_tokens_id": { + "name": "password_reset_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "positions_id": { + "name": "positions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rate_limits_key": { + "name": "rate_limits_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_sessions_id": { + "name": "saml_sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_slo_states_id": { + "name": "saml_slo_states_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_sps_id": { + "name": "saml_sps_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_roles_id": { + "name": "service_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "rotated_at": { + "name": "rotated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "signing_keys_id": { + "name": "signing_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "teams_id": { + "name": "teams_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tenants_id": { + "name": "tenants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_departments_id": { + "name": "user_departments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_parts_id": { + "name": "user_parts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_service_assignments_id": { + "name": "user_service_assignments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_teams_id": { + "name": "user_teams_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_email": { + "name": "pending_email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_email_requested_at": { + "name": "pending_email_requested_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "deletion_scheduled_at": { + "name": "deletion_scheduled_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('ko-KR')" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('Asia/Seoul')" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "users_deletion_gc_idx": { + "name": "users_deletion_gc_idx", + "columns": [ + "status", + "deletion_scheduled_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_challenges_id": { + "name": "webauthn_challenges_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0010_snapshot.json b/drizzle/mysql/meta/0010_snapshot.json new file mode 100644 index 0000000..e3d4f77 --- /dev/null +++ b/drizzle/mysql/meta/0010_snapshot.json @@ -0,0 +1,4270 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "7901ec50-4872-48e0-8306-aa3a079c9d6a", + "prevId": "3621640b-14d8-49c0-8c37-b489e3a48df7", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_events_id": { + "name": "audit_events_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "client_skins_id": { + "name": "client_skins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credentials_id": { + "name": "credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "departments_id": { + "name": "departments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "email_change_tokens": { + "name": "email_change_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_email": { + "name": "target_email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "email_change_tokens_user_idx": { + "name": "email_change_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_change_tokens_hash_uidx": { + "name": "email_change_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_change_tokens_user_id_users_id_fk": { + "name": "email_change_tokens_user_id_users_id_fk", + "tableFrom": "email_change_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "email_change_tokens_id": { + "name": "email_change_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "email_verification_tokens": { + "name": "email_verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "email_verification_tokens_id": { + "name": "email_verification_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "last_login_at": { + "name": "last_login_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "identities_id": { + "name": "identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "identity_providers_id": { + "name": "identity_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "invite_tokens": { + "name": "invite_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "invite_tokens_user_idx": { + "name": "invite_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "invite_tokens_hash_uidx": { + "name": "invite_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invite_tokens_user_id_users_id_fk": { + "name": "invite_tokens_user_id_users_id_fk", + "tableFrom": "invite_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "invite_tokens_id": { + "name": "invite_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('openid')" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('authorization_code,refresh_token')" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('code')" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('RS256')" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "organization_claim_config": { + "name": "organization_claim_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_clients_id": { + "name": "oidc_clients_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_grants_id": { + "name": "oidc_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "oidc_refresh_tokens_id": { + "name": "oidc_refresh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "parts_id": { + "name": "parts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "password_reset_tokens_id": { + "name": "password_reset_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "positions_id": { + "name": "positions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rate_limits_key": { + "name": "rate_limits_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_sessions_id": { + "name": "saml_sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_slo_states_id": { + "name": "saml_slo_states_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saml_sps_id": { + "name": "saml_sps_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_roles_id": { + "name": "service_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "rotated_at": { + "name": "rotated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "signing_keys_id": { + "name": "signing_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "teams_id": { + "name": "teams_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tenants_id": { + "name": "tenants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_departments_id": { + "name": "user_departments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_parts_id": { + "name": "user_parts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_service_assignments_id": { + "name": "user_service_assignments_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "ended_at": { + "name": "ended_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_teams_id": { + "name": "user_teams_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_email": { + "name": "pending_email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_email_requested_at": { + "name": "pending_email_requested_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "deletion_scheduled_at": { + "name": "deletion_scheduled_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('ko-KR')" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('Asia/Seoul')" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + }, + "updated_at": { + "name": "updated_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP(3))" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "users_deletion_gc_idx": { + "name": "users_deletion_gc_idx", + "columns": [ + "status", + "deletion_scheduled_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "datetime(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_challenges_id": { + "name": "webauthn_challenges_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/_journal.json b/drizzle/mysql/meta/_journal.json index 8d9d568..a403c03 100644 --- a/drizzle/mysql/meta/_journal.json +++ b/drizzle/mysql/meta/_journal.json @@ -57,6 +57,27 @@ "when": 1783310297082, "tag": "0007_colorful_felicia_hardy", "breakpoints": true + }, + { + "idx": 8, + "version": "5", + "when": 1783319176779, + "tag": "0008_slow_malcolm_colcord", + "breakpoints": true + }, + { + "idx": 9, + "version": "5", + "when": 1783320562456, + "tag": "0009_previous_sasquatch", + "breakpoints": true + }, + { + "idx": 10, + "version": "5", + "when": 1783323957965, + "tag": "0010_flimsy_phantom_reporter", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/pg/0008_spooky_mindworm.sql b/drizzle/pg/0008_spooky_mindworm.sql new file mode 100644 index 0000000..feb3ff1 --- /dev/null +++ b/drizzle/pg/0008_spooky_mindworm.sql @@ -0,0 +1 @@ +CREATE INDEX "users_deletion_pending_idx" ON "users" USING btree ("deletion_scheduled_at") WHERE "users"."status" = 'deletion_pending'; \ No newline at end of file diff --git a/drizzle/pg/0009_sad_lockjaw.sql b/drizzle/pg/0009_sad_lockjaw.sql new file mode 100644 index 0000000..e3cc481 --- /dev/null +++ b/drizzle/pg/0009_sad_lockjaw.sql @@ -0,0 +1,15 @@ +CREATE TABLE "email_change_tokens" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "token_hash" text NOT NULL, + "target_email" text NOT NULL, + "expires_at" timestamp (3) with time zone NOT NULL, + "used_at" timestamp (3) with time zone, + "created_at" timestamp (3) with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "pending_email" text;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "pending_email_requested_at" timestamp (3) with time zone;--> statement-breakpoint +ALTER TABLE "email_change_tokens" ADD CONSTRAINT "email_change_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "email_change_tokens_user_idx" ON "email_change_tokens" USING btree ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "email_change_tokens_hash_uidx" ON "email_change_tokens" USING btree ("token_hash"); \ No newline at end of file diff --git a/drizzle/pg/0010_cooing_invaders.sql b/drizzle/pg/0010_cooing_invaders.sql new file mode 100644 index 0000000..5fbd40f --- /dev/null +++ b/drizzle/pg/0010_cooing_invaders.sql @@ -0,0 +1 @@ +ALTER TABLE "oidc_clients" ADD COLUMN "organization_claim_config" text; \ No newline at end of file diff --git a/drizzle/pg/meta/0008_snapshot.json b/drizzle/pg/meta/0008_snapshot.json new file mode 100644 index 0000000..452aef1 --- /dev/null +++ b/drizzle/pg/meta/0008_snapshot.json @@ -0,0 +1,4447 @@ +{ + "id": "38b5c818-6517-4bde-a0eb-00776bddf44b", + "prevId": "fdb7d437-2366-427a-be52-240b587a7429", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_events": { + "name": "audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_skins": { + "name": "client_skins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skin_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + { + "expression": "totp_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.departments": { + "name": "departments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_verification_tokens": { + "name": "email_verification_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.identities": { + "name": "identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.identity_providers": { + "name": "identity_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_tokens": { + "name": "invite_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invite_tokens_user_idx": { + "name": "invite_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invite_tokens_hash_uidx": { + "name": "invite_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invite_tokens_user_id_users_id_fk": { + "name": "invite_tokens_user_id_users_id_fk", + "tableFrom": "invite_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_clients": { + "name": "oidc_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_grants": { + "name": "oidc_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.parts": { + "name": "parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_tokens": { + "name": "password_reset_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.positions": { + "name": "positions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limits": { + "name": "rate_limits", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "schema": "", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seen_at": { + "name": "seen_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_sessions": { + "name": "saml_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + { + "expression": "session_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sp_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_slo_states": { + "name": "saml_slo_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_sps": { + "name": "saml_sps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.service_roles": { + "name": "service_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + { + "expression": "idp_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signing_keys": { + "name": "signing_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "not_after": { + "name": "not_after", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"signing_keys\".\"active\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + { + "expression": "department_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenants": { + "name": "tenants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_departments": { + "name": "user_departments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + { + "expression": "department_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_parts": { + "name": "user_parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + { + "expression": "part_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_service_assignments": { + "name": "user_service_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_teams": { + "name": "user_teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "deletion_scheduled_at": { + "name": "deletion_scheduled_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_deletion_pending_idx": { + "name": "users_deletion_pending_idx", + "columns": [ + { + "expression": "deletion_scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"status\" = 'deletion_pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_challenges": { + "name": "webauthn_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "challenge", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/pg/meta/0009_snapshot.json b/drizzle/pg/meta/0009_snapshot.json new file mode 100644 index 0000000..56a8fb2 --- /dev/null +++ b/drizzle/pg/meta/0009_snapshot.json @@ -0,0 +1,4560 @@ +{ + "id": "60eed18b-07c8-4303-b32e-17f7bba5519f", + "prevId": "38b5c818-6517-4bde-a0eb-00776bddf44b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_events": { + "name": "audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_skins": { + "name": "client_skins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skin_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + { + "expression": "totp_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.departments": { + "name": "departments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_change_tokens": { + "name": "email_change_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_email": { + "name": "target_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_change_tokens_user_idx": { + "name": "email_change_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_change_tokens_hash_uidx": { + "name": "email_change_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_change_tokens_user_id_users_id_fk": { + "name": "email_change_tokens_user_id_users_id_fk", + "tableFrom": "email_change_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_verification_tokens": { + "name": "email_verification_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.identities": { + "name": "identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.identity_providers": { + "name": "identity_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_tokens": { + "name": "invite_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invite_tokens_user_idx": { + "name": "invite_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invite_tokens_hash_uidx": { + "name": "invite_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invite_tokens_user_id_users_id_fk": { + "name": "invite_tokens_user_id_users_id_fk", + "tableFrom": "invite_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_clients": { + "name": "oidc_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_grants": { + "name": "oidc_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.parts": { + "name": "parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_tokens": { + "name": "password_reset_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.positions": { + "name": "positions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limits": { + "name": "rate_limits", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "schema": "", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seen_at": { + "name": "seen_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_sessions": { + "name": "saml_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + { + "expression": "session_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sp_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_slo_states": { + "name": "saml_slo_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_sps": { + "name": "saml_sps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.service_roles": { + "name": "service_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + { + "expression": "idp_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signing_keys": { + "name": "signing_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "not_after": { + "name": "not_after", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"signing_keys\".\"active\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + { + "expression": "department_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenants": { + "name": "tenants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_departments": { + "name": "user_departments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + { + "expression": "department_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_parts": { + "name": "user_parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + { + "expression": "part_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_service_assignments": { + "name": "user_service_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_teams": { + "name": "user_teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "pending_email": { + "name": "pending_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pending_email_requested_at": { + "name": "pending_email_requested_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "deletion_scheduled_at": { + "name": "deletion_scheduled_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_deletion_pending_idx": { + "name": "users_deletion_pending_idx", + "columns": [ + { + "expression": "deletion_scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"status\" = 'deletion_pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_challenges": { + "name": "webauthn_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "challenge", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/pg/meta/0010_snapshot.json b/drizzle/pg/meta/0010_snapshot.json new file mode 100644 index 0000000..6fcb706 --- /dev/null +++ b/drizzle/pg/meta/0010_snapshot.json @@ -0,0 +1,4566 @@ +{ + "id": "d67dcb12-313e-497d-bb01-2684ca87529a", + "prevId": "60eed18b-07c8-4303-b32e-17f7bba5519f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_events": { + "name": "audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_skins": { + "name": "client_skins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skin_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + { + "expression": "totp_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.departments": { + "name": "departments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_change_tokens": { + "name": "email_change_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_email": { + "name": "target_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_change_tokens_user_idx": { + "name": "email_change_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_change_tokens_hash_uidx": { + "name": "email_change_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_change_tokens_user_id_users_id_fk": { + "name": "email_change_tokens_user_id_users_id_fk", + "tableFrom": "email_change_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_verification_tokens": { + "name": "email_verification_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.identities": { + "name": "identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.identity_providers": { + "name": "identity_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_tokens": { + "name": "invite_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invite_tokens_user_idx": { + "name": "invite_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invite_tokens_hash_uidx": { + "name": "invite_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invite_tokens_user_id_users_id_fk": { + "name": "invite_tokens_user_id_users_id_fk", + "tableFrom": "invite_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_clients": { + "name": "oidc_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_claim_config": { + "name": "organization_claim_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_grants": { + "name": "oidc_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.parts": { + "name": "parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_tokens": { + "name": "password_reset_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.positions": { + "name": "positions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limits": { + "name": "rate_limits", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "schema": "", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seen_at": { + "name": "seen_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_sessions": { + "name": "saml_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + { + "expression": "session_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sp_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_slo_states": { + "name": "saml_slo_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saml_sps": { + "name": "saml_sps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.service_roles": { + "name": "service_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + { + "expression": "idp_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signing_keys": { + "name": "signing_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "not_after": { + "name": "not_after", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"signing_keys\".\"active\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + { + "expression": "department_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenants": { + "name": "tenants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_departments": { + "name": "user_departments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + { + "expression": "department_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_parts": { + "name": "user_parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + { + "expression": "part_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_service_assignments": { + "name": "user_service_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "service_ref_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_teams": { + "name": "user_teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "pending_email": { + "name": "pending_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pending_email_requested_at": { + "name": "pending_email_requested_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "deletion_scheduled_at": { + "name": "deletion_scheduled_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_deletion_pending_idx": { + "name": "users_deletion_pending_idx", + "columns": [ + { + "expression": "deletion_scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"users\".\"status\" = 'deletion_pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_challenges": { + "name": "webauthn_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "challenge", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/pg/meta/_journal.json b/drizzle/pg/meta/_journal.json index 2c3e7bb..e5fbc21 100644 --- a/drizzle/pg/meta/_journal.json +++ b/drizzle/pg/meta/_journal.json @@ -57,6 +57,27 @@ "when": 1783310296760, "tag": "0007_sudden_loki", "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1783319176375, + "tag": "0008_spooky_mindworm", + "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1783320562110, + "tag": "0009_sad_lockjaw", + "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1783323957626, + "tag": "0010_cooing_invaders", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/sqlite/0008_free_spyke.sql b/drizzle/sqlite/0008_free_spyke.sql new file mode 100644 index 0000000..95101b3 --- /dev/null +++ b/drizzle/sqlite/0008_free_spyke.sql @@ -0,0 +1 @@ +CREATE INDEX `users_deletion_pending_idx` ON `users` (`deletion_scheduled_at`) WHERE status = 'deletion_pending'; \ No newline at end of file diff --git a/drizzle/sqlite/0009_slim_the_order.sql b/drizzle/sqlite/0009_slim_the_order.sql new file mode 100644 index 0000000..2c8cc65 --- /dev/null +++ b/drizzle/sqlite/0009_slim_the_order.sql @@ -0,0 +1,15 @@ +CREATE TABLE `email_change_tokens` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `token_hash` text NOT NULL, + `target_email` text NOT NULL, + `expires_at` integer NOT NULL, + `used_at` integer, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `email_change_tokens_user_idx` ON `email_change_tokens` (`user_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `email_change_tokens_hash_uidx` ON `email_change_tokens` (`token_hash`);--> statement-breakpoint +ALTER TABLE `users` ADD `pending_email` text;--> statement-breakpoint +ALTER TABLE `users` ADD `pending_email_requested_at` integer; \ No newline at end of file diff --git a/drizzle/sqlite/0010_silent_the_call.sql b/drizzle/sqlite/0010_silent_the_call.sql new file mode 100644 index 0000000..3556bf7 --- /dev/null +++ b/drizzle/sqlite/0010_silent_the_call.sql @@ -0,0 +1 @@ +ALTER TABLE `oidc_clients` ADD `organization_claim_config` text; \ No newline at end of file diff --git a/drizzle/sqlite/meta/0008_snapshot.json b/drizzle/sqlite/meta/0008_snapshot.json new file mode 100644 index 0000000..462714f --- /dev/null +++ b/drizzle/sqlite/meta/0008_snapshot.json @@ -0,0 +1,3958 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "40ebab7c-e675-4cec-8430-57e8922ed6f0", + "prevId": "d8ea7d7a-ad05-464e-aeeb-910f5ffc7c52", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_verification_tokens": { + "name": "email_verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_login_at": { + "name": "last_login_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite_tokens": { + "name": "invite_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "invite_tokens_user_idx": { + "name": "invite_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "invite_tokens_hash_uidx": { + "name": "invite_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invite_tokens_user_id_users_id_fk": { + "name": "invite_tokens_user_id_users_id_fk", + "tableFrom": "invite_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "rotated_at": { + "name": "rotated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + "tenant_id" + ], + "isUnique": true, + "where": "active = 1" + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "deletion_scheduled_at": { + "name": "deletion_scheduled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "users_deletion_pending_idx": { + "name": "users_deletion_pending_idx", + "columns": [ + "deletion_scheduled_at" + ], + "isUnique": false, + "where": "status = 'deletion_pending'" + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0009_snapshot.json b/drizzle/sqlite/meta/0009_snapshot.json new file mode 100644 index 0000000..3be3157 --- /dev/null +++ b/drizzle/sqlite/meta/0009_snapshot.json @@ -0,0 +1,4061 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "5a8d0604-ca48-407b-a9ef-2427e0293649", + "prevId": "40ebab7c-e675-4cec-8430-57e8922ed6f0", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_change_tokens": { + "name": "email_change_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_email": { + "name": "target_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "email_change_tokens_user_idx": { + "name": "email_change_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_change_tokens_hash_uidx": { + "name": "email_change_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_change_tokens_user_id_users_id_fk": { + "name": "email_change_tokens_user_id_users_id_fk", + "tableFrom": "email_change_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_verification_tokens": { + "name": "email_verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_login_at": { + "name": "last_login_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite_tokens": { + "name": "invite_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "invite_tokens_user_idx": { + "name": "invite_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "invite_tokens_hash_uidx": { + "name": "invite_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invite_tokens_user_id_users_id_fk": { + "name": "invite_tokens_user_id_users_id_fk", + "tableFrom": "invite_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "rotated_at": { + "name": "rotated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + "tenant_id" + ], + "isUnique": true, + "where": "active = 1" + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_email": { + "name": "pending_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_email_requested_at": { + "name": "pending_email_requested_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "deletion_scheduled_at": { + "name": "deletion_scheduled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "users_deletion_pending_idx": { + "name": "users_deletion_pending_idx", + "columns": [ + "deletion_scheduled_at" + ], + "isUnique": false, + "where": "status = 'deletion_pending'" + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0010_snapshot.json b/drizzle/sqlite/meta/0010_snapshot.json new file mode 100644 index 0000000..84d2a38 --- /dev/null +++ b/drizzle/sqlite/meta/0010_snapshot.json @@ -0,0 +1,4068 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "a8b68942-c72d-4210-b48b-993b48dcbfa7", + "prevId": "5a8d0604-ca48-407b-a9ef-2427e0293649", + "tables": { + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sp_or_client_id": { + "name": "sp_or_client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail_json": { + "name": "detail_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "audit_events_tenant_kind_idx": { + "name": "audit_events_tenant_kind_idx", + "columns": [ + "tenant_id", + "kind" + ], + "isUnique": false + }, + "audit_events_tenant_created_idx": { + "name": "audit_events_tenant_created_idx", + "columns": [ + "tenant_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_idx": { + "name": "audit_events_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_events_tenant_id_tenants_id_fk": { + "name": "audit_events_tenant_id_tenants_id_fk", + "tableFrom": "audit_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "client_skins": { + "name": "client_skins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_type": { + "name": "client_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_ref_id": { + "name": "client_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skin_type": { + "name": "skin_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'login'" + }, + "fetch_url": { + "name": "fetch_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fetch_secret": { + "name": "fetch_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_ttl_seconds": { + "name": "cache_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3600 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_skins_unique": { + "name": "client_skins_unique", + "columns": [ + "tenant_id", + "client_type", + "client_ref_id", + "skin_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "client_skins_tenant_id_tenants_id_fk": { + "name": "client_skins_tenant_id_tenants_id_fk", + "tableFrom": "client_skins", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credentials": { + "name": "credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_owner_id": { + "name": "totp_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "credentials_user_idx": { + "name": "credentials_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "credentials_user_type_idx": { + "name": "credentials_user_type_idx", + "columns": [ + "user_id", + "type" + ], + "isUnique": false + }, + "credentials_webauthn_credential_id_uidx": { + "name": "credentials_webauthn_credential_id_uidx", + "columns": [ + "credential_id" + ], + "isUnique": true + }, + "credentials_totp_owner_uidx": { + "name": "credentials_totp_owner_uidx", + "columns": [ + "totp_owner_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "departments": { + "name": "departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "departments_tenant_idx": { + "name": "departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "departments_parent_idx": { + "name": "departments_parent_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "departments_tenant_code_uidx": { + "name": "departments_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "departments_tenant_id_tenants_id_fk": { + "name": "departments_tenant_id_tenants_id_fk", + "tableFrom": "departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "departments_parent_id_departments_id_fk": { + "name": "departments_parent_id_departments_id_fk", + "tableFrom": "departments", + "tableTo": "departments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "departments_manager_id_users_id_fk": { + "name": "departments_manager_id_users_id_fk", + "tableFrom": "departments", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_change_tokens": { + "name": "email_change_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_email": { + "name": "target_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "email_change_tokens_user_idx": { + "name": "email_change_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_change_tokens_hash_uidx": { + "name": "email_change_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_change_tokens_user_id_users_id_fk": { + "name": "email_change_tokens_user_id_users_id_fk", + "tableFrom": "email_change_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "email_verification_tokens": { + "name": "email_verification_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "email_verification_tokens_user_idx": { + "name": "email_verification_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "email_verification_tokens_hash_uidx": { + "name": "email_verification_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "email_verification_tokens_user_id_users_id_fk": { + "name": "email_verification_tokens_user_id_users_id_fk", + "tableFrom": "email_verification_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identities": { + "name": "identities", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_profile_json": { + "name": "raw_profile_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_login_at": { + "name": "last_login_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "identities_tenant_provider_subject_uidx": { + "name": "identities_tenant_provider_subject_uidx", + "columns": [ + "tenant_id", + "provider", + "subject" + ], + "isUnique": true + }, + "identities_user_idx": { + "name": "identities_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identities_tenant_id_tenants_id_fk": { + "name": "identities_tenant_id_tenants_id_fk", + "tableFrom": "identities", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "identities_user_id_users_id_fk": { + "name": "identities_user_id_users_id_fk", + "tableFrom": "identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "identity_providers": { + "name": "identity_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret_enc": { + "name": "client_secret_enc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "discovery_url": { + "name": "discovery_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata_xml": { + "name": "metadata_xml", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "idp_tenant_idx": { + "name": "idp_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "idp_tenant_name_uidx": { + "name": "idp_tenant_name_uidx", + "columns": [ + "tenant_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "identity_providers_tenant_id_tenants_id_fk": { + "name": "identity_providers_tenant_id_tenants_id_fk", + "tableFrom": "identity_providers", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite_tokens": { + "name": "invite_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "invite_tokens_user_idx": { + "name": "invite_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "invite_tokens_hash_uidx": { + "name": "invite_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invite_tokens_user_id_users_id_fk": { + "name": "invite_tokens_user_id_users_id_fk", + "tableFrom": "invite_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_clients": { + "name": "oidc_clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_uri": { + "name": "frontchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frontchannel_logout_session_required": { + "name": "frontchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openid'" + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'authorization_code,refresh_token'" + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'code'" + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'client_secret_basic'" + }, + "require_pkce": { + "name": "require_pkce", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_wildcard_redirect_uri": { + "name": "allow_wildcard_redirect_uri", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "id_token_signed_response_alg": { + "name": "id_token_signed_response_alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'RS256'" + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "organization_claim_config": { + "name": "organization_claim_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_clients_tenant_client_id_uidx": { + "name": "oidc_clients_tenant_client_id_uidx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": true + }, + "oidc_clients_tenant_idx": { + "name": "oidc_clients_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_clients_tenant_id_tenants_id_fk": { + "name": "oidc_clients_tenant_id_tenants_id_fk", + "tableFrom": "oidc_clients", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_grants": { + "name": "oidc_grants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_grants_code_hash_uidx": { + "name": "oidc_grants_code_hash_uidx", + "columns": [ + "code_hash" + ], + "isUnique": true + }, + "oidc_grants_tenant_client_idx": { + "name": "oidc_grants_tenant_client_idx", + "columns": [ + "tenant_id", + "client_id" + ], + "isUnique": false + }, + "oidc_grants_expires_idx": { + "name": "oidc_grants_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_grants_tenant_id_tenants_id_fk": { + "name": "oidc_grants_tenant_id_tenants_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_user_id_users_id_fk": { + "name": "oidc_grants_user_id_users_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_grants_session_id_sessions_id_fk": { + "name": "oidc_grants_session_id_sessions_id_fk", + "tableFrom": "oidc_grants", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_refresh_tokens": { + "name": "oidc_refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "replaced_by_id": { + "name": "replaced_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "oidc_refresh_tokens_hash_uidx": { + "name": "oidc_refresh_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "oidc_refresh_tokens_user_idx": { + "name": "oidc_refresh_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "oidc_refresh_tokens_tenant_id_tenants_id_fk": { + "name": "oidc_refresh_tokens_tenant_id_tenants_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_user_id_users_id_fk": { + "name": "oidc_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oidc_refresh_tokens_session_id_sessions_id_fk": { + "name": "oidc_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oidc_refresh_tokens", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "parts": { + "name": "parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "parts_tenant_idx": { + "name": "parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "parts_team_idx": { + "name": "parts_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "parts_tenant_code_uidx": { + "name": "parts_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "parts_tenant_id_tenants_id_fk": { + "name": "parts_tenant_id_tenants_id_fk", + "tableFrom": "parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "parts_team_id_teams_id_fk": { + "name": "parts_team_id_teams_id_fk", + "tableFrom": "parts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "parts_leader_id_users_id_fk": { + "name": "parts_leader_id_users_id_fk", + "tableFrom": "parts", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "password_reset_tokens": { + "name": "password_reset_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "password_reset_tokens_user_idx": { + "name": "password_reset_tokens_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "password_reset_tokens_hash_uidx": { + "name": "password_reset_tokens_hash_uidx", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "password_reset_tokens_user_id_users_id_fk": { + "name": "password_reset_tokens_user_id_users_id_fk", + "tableFrom": "password_reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "positions": { + "name": "positions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "positions_tenant_idx": { + "name": "positions_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "positions_tenant_code_uidx": { + "name": "positions_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "positions_tenant_id_tenants_id_fk": { + "name": "positions_tenant_id_tenants_id_fk", + "tableFrom": "positions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_authn_request_ids": { + "name": "saml_authn_request_ids", + "columns": { + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_entity_id": { + "name": "sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seen_at": { + "name": "seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saml_authn_request_ids_tenant_req_uidx": { + "name": "saml_authn_request_ids_tenant_req_uidx", + "columns": [ + "tenant_id", + "request_id" + ], + "isUnique": true + }, + "saml_authn_request_ids_expires_idx": { + "name": "saml_authn_request_ids_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_authn_request_ids_tenant_id_tenants_id_fk": { + "name": "saml_authn_request_ids_tenant_id_tenants_id_fk", + "tableFrom": "saml_authn_request_ids", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sessions": { + "name": "saml_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sp_id": { + "name": "sp_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_index": { + "name": "session_index", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id": { + "name": "name_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_on_or_after": { + "name": "not_on_or_after", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "saml_sessions_session_index_uidx": { + "name": "saml_sessions_session_index_uidx", + "columns": [ + "session_index" + ], + "isUnique": true + }, + "saml_sessions_tenant_sp_idx": { + "name": "saml_sessions_tenant_sp_idx", + "columns": [ + "tenant_id", + "sp_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sessions_tenant_id_tenants_id_fk": { + "name": "saml_sessions_tenant_id_tenants_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_sp_id_saml_sps_id_fk": { + "name": "saml_sessions_sp_id_saml_sps_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "saml_sps", + "columnsFrom": [ + "sp_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_user_id_users_id_fk": { + "name": "saml_sessions_user_id_users_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_sessions_session_id_sessions_id_fk": { + "name": "saml_sessions_session_id_sessions_id_fk", + "tableFrom": "saml_sessions", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_slo_states": { + "name": "saml_slo_states", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_record_id": { + "name": "idp_session_record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiating_sp_entity_id": { + "name": "initiating_sp_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_response_to": { + "name": "in_response_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiator_slo_url": { + "name": "initiator_slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completion_uri": { + "name": "completion_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_sp_data_json": { + "name": "pending_sp_data_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "saml_slo_states_tenant_id_tenants_id_fk": { + "name": "saml_slo_states_tenant_id_tenants_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saml_slo_states_user_id_users_id_fk": { + "name": "saml_slo_states_user_id_users_id_fk", + "tableFrom": "saml_slo_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saml_sps": { + "name": "saml_sps", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_url": { + "name": "acs_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acs_binding": { + "name": "acs_binding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'" + }, + "slo_url": { + "name": "slo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slo_binding": { + "name": "slo_binding", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert": { + "name": "cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_id_format": { + "name": "name_id_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'" + }, + "sign_assertion": { + "name": "sign_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sign_response": { + "name": "sign_response", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "encrypt_assertion": { + "name": "encrypt_assertion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "want_authn_requests_signed": { + "name": "want_authn_requests_signed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attribute_mapping_json": { + "name": "attribute_mapping_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_attributes": { + "name": "allowed_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "saml_sps_tenant_entity_id_uidx": { + "name": "saml_sps_tenant_entity_id_uidx", + "columns": [ + "tenant_id", + "entity_id" + ], + "isUnique": true + }, + "saml_sps_tenant_idx": { + "name": "saml_sps_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saml_sps_tenant_id_tenants_id_fk": { + "name": "saml_sps_tenant_id_tenants_id_fk", + "tableFrom": "saml_sps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_roles": { + "name": "service_roles", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "service_roles_service_key_uidx": { + "name": "service_roles_service_key_uidx", + "columns": [ + "service_type", + "service_ref_id", + "key" + ], + "isUnique": true + }, + "service_roles_tenant_service_idx": { + "name": "service_roles_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "service_roles_tenant_id_tenants_id_fk": { + "name": "service_roles_tenant_id_tenants_id_fk", + "tableFrom": "service_roles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idp_session_id": { + "name": "idp_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amr": { + "name": "amr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acr": { + "name": "acr", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_idp_session_id_uidx": { + "name": "sessions_idp_session_id_uidx", + "columns": [ + "idp_session_id" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_idx": { + "name": "sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_tenant_id_tenants_id_fk": { + "name": "sessions_tenant_id_tenants_id_fk", + "tableFrom": "sessions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "signing_keys": { + "name": "signing_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kid": { + "name": "kid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use": { + "name": "use", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'sig'" + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_jwk": { + "name": "public_jwk", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_jwk_encrypted": { + "name": "private_jwk_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "rotated_at": { + "name": "rotated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "not_after": { + "name": "not_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "signing_keys_tenant_kid_uidx": { + "name": "signing_keys_tenant_kid_uidx", + "columns": [ + "tenant_id", + "kid" + ], + "isUnique": true + }, + "signing_keys_tenant_active_idx": { + "name": "signing_keys_tenant_active_idx", + "columns": [ + "tenant_id", + "active" + ], + "isUnique": false + }, + "signing_keys_tenant_one_active_uidx": { + "name": "signing_keys_tenant_one_active_uidx", + "columns": [ + "tenant_id" + ], + "isUnique": true, + "where": "active = 1" + } + }, + "foreignKeys": { + "signing_keys_tenant_id_tenants_id_fk": { + "name": "signing_keys_tenant_id_tenants_id_fk", + "tableFrom": "signing_keys", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "leader_id": { + "name": "leader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "teams_tenant_idx": { + "name": "teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "teams_department_idx": { + "name": "teams_department_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "teams_tenant_code_uidx": { + "name": "teams_tenant_code_uidx", + "columns": [ + "tenant_id", + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "teams_tenant_id_tenants_id_fk": { + "name": "teams_tenant_id_tenants_id_fk", + "tableFrom": "teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "teams_department_id_departments_id_fk": { + "name": "teams_department_id_departments_id_fk", + "tableFrom": "teams", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "teams_leader_id_users_id_fk": { + "name": "teams_leader_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tenants": { + "name": "tenants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "tenants_slug_uidx": { + "name": "tenants_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_departments": { + "name": "user_departments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "department_id": { + "name": "department_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_departments_user_idx": { + "name": "user_departments_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_departments_dept_idx": { + "name": "user_departments_dept_idx", + "columns": [ + "department_id" + ], + "isUnique": false + }, + "user_departments_tenant_idx": { + "name": "user_departments_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_departments_tenant_id_tenants_id_fk": { + "name": "user_departments_tenant_id_tenants_id_fk", + "tableFrom": "user_departments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_user_id_users_id_fk": { + "name": "user_departments_user_id_users_id_fk", + "tableFrom": "user_departments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_department_id_departments_id_fk": { + "name": "user_departments_department_id_departments_id_fk", + "tableFrom": "user_departments", + "tableTo": "departments", + "columnsFrom": [ + "department_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_departments_position_id_positions_id_fk": { + "name": "user_departments_position_id_positions_id_fk", + "tableFrom": "user_departments", + "tableTo": "positions", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_parts": { + "name": "user_parts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_id": { + "name": "part_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_parts_user_idx": { + "name": "user_parts_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_parts_part_idx": { + "name": "user_parts_part_idx", + "columns": [ + "part_id" + ], + "isUnique": false + }, + "user_parts_tenant_idx": { + "name": "user_parts_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_parts_tenant_id_tenants_id_fk": { + "name": "user_parts_tenant_id_tenants_id_fk", + "tableFrom": "user_parts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_user_id_users_id_fk": { + "name": "user_parts_user_id_users_id_fk", + "tableFrom": "user_parts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_parts_part_id_parts_id_fk": { + "name": "user_parts_part_id_parts_id_fk", + "tableFrom": "user_parts", + "tableTo": "parts", + "columnsFrom": [ + "part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_service_assignments": { + "name": "user_service_assignments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_type": { + "name": "service_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_ref_id": { + "name": "service_ref_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_role_id": { + "name": "service_role_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_service_assignments_user_service_uidx": { + "name": "user_service_assignments_user_service_uidx", + "columns": [ + "tenant_id", + "user_id", + "service_type", + "service_ref_id" + ], + "isUnique": true + }, + "user_service_assignments_tenant_user_idx": { + "name": "user_service_assignments_tenant_user_idx", + "columns": [ + "tenant_id", + "user_id" + ], + "isUnique": false + }, + "user_service_assignments_tenant_service_idx": { + "name": "user_service_assignments_tenant_service_idx", + "columns": [ + "tenant_id", + "service_type", + "service_ref_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_service_assignments_tenant_id_tenants_id_fk": { + "name": "user_service_assignments_tenant_id_tenants_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_user_id_users_id_fk": { + "name": "user_service_assignments_user_id_users_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_service_assignments_service_role_id_service_roles_id_fk": { + "name": "user_service_assignments_service_role_id_service_roles_id_fk", + "tableFrom": "user_service_assignments", + "tableTo": "service_roles", + "columnsFrom": [ + "service_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_teams": { + "name": "user_teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "user_teams_user_idx": { + "name": "user_teams_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "user_teams_team_idx": { + "name": "user_teams_team_idx", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "user_teams_tenant_idx": { + "name": "user_teams_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_teams_tenant_id_tenants_id_fk": { + "name": "user_teams_tenant_id_tenants_id_fk", + "tableFrom": "user_teams", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_email": { + "name": "pending_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_email_requested_at": { + "name": "pending_email_requested_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "deletion_scheduled_at": { + "name": "deletion_scheduled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_verified_at": { + "name": "phone_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'ko-KR'" + }, + "zoneinfo": { + "name": "zoneinfo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'Asia/Seoul'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "birthdate": { + "name": "birthdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_locality": { + "name": "address_locality", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_region": { + "name": "address_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_postal_code": { + "name": "address_postal_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_tenant_email_uidx": { + "name": "users_tenant_email_uidx", + "columns": [ + "tenant_id", + "email" + ], + "isUnique": true + }, + "users_tenant_username_uidx": { + "name": "users_tenant_username_uidx", + "columns": [ + "tenant_id", + "username" + ], + "isUnique": true + }, + "users_tenant_idx": { + "name": "users_tenant_idx", + "columns": [ + "tenant_id" + ], + "isUnique": false + }, + "users_deletion_pending_idx": { + "name": "users_deletion_pending_idx", + "columns": [ + "deletion_scheduled_at" + ], + "isUnique": false, + "where": "status = 'deletion_pending'" + } + }, + "foreignKeys": { + "users_tenant_id_tenants_id_fk": { + "name": "users_tenant_id_tenants_id_fk", + "tableFrom": "users", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_challenges": { + "name": "webauthn_challenges", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webauthn_challenges_tenant_challenge_uidx": { + "name": "webauthn_challenges_tenant_challenge_uidx", + "columns": [ + "tenant_id", + "challenge" + ], + "isUnique": true + }, + "webauthn_challenges_tenant_expires_idx": { + "name": "webauthn_challenges_tenant_expires_idx", + "columns": [ + "tenant_id", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "webauthn_challenges_tenant_id_tenants_id_fk": { + "name": "webauthn_challenges_tenant_id_tenants_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webauthn_challenges_user_id_users_id_fk": { + "name": "webauthn_challenges_user_id_users_id_fk", + "tableFrom": "webauthn_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/_journal.json b/drizzle/sqlite/meta/_journal.json index 72359e0..14130a4 100644 --- a/drizzle/sqlite/meta/_journal.json +++ b/drizzle/sqlite/meta/_journal.json @@ -57,6 +57,27 @@ "when": 1783310297361, "tag": "0007_calm_proudstar", "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1783319177083, + "tag": "0008_free_spyke", + "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1783320562744, + "tag": "0009_slim_the_order", + "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1783323958251, + "tag": "0010_silent_the_call", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/server/db/schema.mysql.ts b/src/lib/server/db/schema.mysql.ts index 913717e..50b86d1 100644 --- a/src/lib/server/db/schema.mysql.ts +++ b/src/lib/server/db/schema.mysql.ts @@ -38,6 +38,11 @@ export const users = mysqlTable( username: varchar("username", { length: 255 }), email: varchar("email", { length: 320 }).notNull(), emailVerifiedAt: datetime("email_verified_at", { mode: "date", fsp: 3 }), + // F3: 이메일 변경 대기 상태. 새 주소 확인(email_change_tokens) 전까지 여기 보관하고, + // 확인 완료 시 email 로 승격 후 NULL 로 클리어한다. requestedAt 은 대기 시작 시각. + // (email 과 동일 계열 타입 유지를 위해 varchar(320) — parity 는 string 계열로 정규화됨.) + pendingEmail: varchar("pending_email", { length: 320 }), + pendingEmailRequestedAt: datetime("pending_email_requested_at", { mode: "date", fsp: 3 }), displayName: text("display_name"), role: varchar("role", { length: 64, enum: ["admin", "user"] }) .notNull() @@ -72,7 +77,15 @@ export const users = mysqlTable( .notNull() .default(sql`(CURRENT_TIMESTAMP(3))`), }, - (t) => [uniqueIndex("users_tenant_email_uidx").on(t.tenantId, t.email), uniqueIndex("users_tenant_username_uidx").on(t.tenantId, t.username), index("users_tenant_idx").on(t.tenantId)], + (t) => [ + uniqueIndex("users_tenant_email_uidx").on(t.tenantId, t.email), + uniqueIndex("users_tenant_username_uidx").on(t.tenantId, t.username), + index("users_tenant_idx").on(t.tenantId), + // GC(하드삭제) 조회 지원. MySQL 은 부분(WHERE) 인덱스를 지원하지 않으므로 sqlite/pg 의 + // 부분 인덱스(users_deletion_pending_idx) 대신 (status, deletionScheduledAt) 복합 인덱스로 + // 동일 조회(status='deletion_pending' & deletionScheduledAt < now)를 지원한다. parity 예외 등재. + index("users_deletion_gc_idx").on(t.status, t.deletionScheduledAt), + ], ); /** @@ -251,6 +264,9 @@ export const oidcClients = mysqlTable( idTokenSignedResponseAlg: text("id_token_signed_response_alg").notNull().default("RS256"), jwksUri: text("jwks_uri"), jwks: text("jwks"), + // organization scope 클레임의 클라이언트별 노출 토글(JSON). null=미설정=전량 노출(하위호환). + // 예: {"department":true,"team":true,"position":false,"jobTitle":true} + organizationClaimConfig: text("organization_claim_config"), enabled: boolean("enabled").notNull().default(true), createdAt: datetime("created_at", { mode: "date", fsp: 3 }) .notNull() @@ -940,6 +956,33 @@ export const inviteTokens = mysqlTable( export type InviteToken = typeof inviteTokens.$inferSelect; +// ---------- Email change ---------- +// F3: 프로필 이메일 변경 확인 토큰. email_verification_tokens 와 분리한다 — 변경 대상 주소 +// (targetEmail)를 토큰에 바인딩해야 하고(확인 링크가 다른 주소로 재사용되지 않도록), 확인 +// 라우트/시맨틱도 다르기 때문이다. SHA-256 해시 저장, TTL 24시간, 1회용(usedAt). +export const emailChangeTokens = mysqlTable( + "email_change_tokens", + { + id: varchar("id", { length: 64 }) + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + userId: varchar("user_id", { length: 64 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + tokenHash: varchar("token_hash", { length: 255 }).notNull(), + // 변경하려는 새 이메일 주소(토큰에 바인딩). 확인 시 이 값으로 users.email 을 교체한다. + targetEmail: varchar("target_email", { length: 320 }).notNull(), + expiresAt: datetime("expires_at", { mode: "date", fsp: 3 }).notNull(), + usedAt: datetime("used_at", { mode: "date", fsp: 3 }), + createdAt: datetime("created_at", { mode: "date", fsp: 3 }) + .notNull() + .default(sql`(CURRENT_TIMESTAMP(3))`), + }, + (t) => [index("email_change_tokens_user_idx").on(t.userId), uniqueIndex("email_change_tokens_hash_uidx").on(t.tokenHash)], +); + +export type EmailChangeToken = typeof emailChangeTokens.$inferSelect; + export type User = typeof users.$inferSelect; export type Credential = typeof credentials.$inferSelect; export type Identity = typeof identities.$inferSelect; diff --git a/src/lib/server/db/schema.pg.ts b/src/lib/server/db/schema.pg.ts index d61a453..6852313 100644 --- a/src/lib/server/db/schema.pg.ts +++ b/src/lib/server/db/schema.pg.ts @@ -34,6 +34,10 @@ export const users = pgTable( username: text("username"), email: text("email").notNull(), emailVerifiedAt: timestamp("email_verified_at", { mode: "date", withTimezone: true, precision: 3 }), + // F3: 이메일 변경 대기 상태. 새 주소 확인(email_change_tokens) 전까지 여기 보관하고, + // 확인 완료 시 email 로 승격 후 NULL 로 클리어한다. requestedAt 은 대기 시작 시각. + pendingEmail: text("pending_email"), + pendingEmailRequestedAt: timestamp("pending_email_requested_at", { mode: "date", withTimezone: true, precision: 3 }), displayName: text("display_name"), role: text("role", { enum: ["admin", "user"] }) .notNull() @@ -63,7 +67,17 @@ export const users = pgTable( createdAt: timestamp("created_at", { mode: "date", withTimezone: true, precision: 3 }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { mode: "date", withTimezone: true, precision: 3 }).notNull().defaultNow(), }, - (t) => [uniqueIndex("users_tenant_email_uidx").on(t.tenantId, t.email), uniqueIndex("users_tenant_username_uidx").on(t.tenantId, t.username), index("users_tenant_idx").on(t.tenantId)], + (t) => [ + uniqueIndex("users_tenant_email_uidx").on(t.tenantId, t.email), + uniqueIndex("users_tenant_username_uidx").on(t.tenantId, t.username), + index("users_tenant_idx").on(t.tenantId), + // GC(하드삭제) 조회 지원 — status='deletion_pending' & deletionScheduledAt 경과분만 스캔한다. + // 부분 인덱스(WHERE status='deletion_pending')로 삭제 예정 계정만 색인해 공간을 아낀다. + // (mysql 은 부분 인덱스 미지원 → users_deletion_gc_idx 복합 인덱스로 대체; parity 예외 등재.) + index("users_deletion_pending_idx") + .on(t.deletionScheduledAt) + .where(sql`${t.status} = 'deletion_pending'`), + ], ); /** @@ -227,6 +241,9 @@ export const oidcClients = pgTable( idTokenSignedResponseAlg: text("id_token_signed_response_alg").notNull().default("RS256"), jwksUri: text("jwks_uri"), jwks: text("jwks"), + // organization scope 클레임의 클라이언트별 노출 토글(JSON). null=미설정=전량 노출(하위호환). + // 예: {"department":true,"team":true,"position":false,"jobTitle":true} + organizationClaimConfig: text("organization_claim_config"), enabled: boolean("enabled").notNull().default(true), createdAt: timestamp("created_at", { mode: "date", withTimezone: true, precision: 3 }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { mode: "date", withTimezone: true, precision: 3 }).notNull().defaultNow(), @@ -854,6 +871,31 @@ export const inviteTokens = pgTable( export type InviteToken = typeof inviteTokens.$inferSelect; +// ---------- Email change ---------- +// F3: 프로필 이메일 변경 확인 토큰. email_verification_tokens 와 분리한다 — 변경 대상 주소 +// (targetEmail)를 토큰에 바인딩해야 하고(확인 링크가 다른 주소로 재사용되지 않도록), 확인 +// 라우트/시맨틱도 다르기 때문이다. SHA-256 해시 저장, TTL 24시간, 1회용(usedAt). +export const emailChangeTokens = pgTable( + "email_change_tokens", + { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + tokenHash: text("token_hash").notNull(), + // 변경하려는 새 이메일 주소(토큰에 바인딩). 확인 시 이 값으로 users.email 을 교체한다. + targetEmail: text("target_email").notNull(), + expiresAt: timestamp("expires_at", { mode: "date", withTimezone: true, precision: 3 }).notNull(), + usedAt: timestamp("used_at", { mode: "date", withTimezone: true, precision: 3 }), + createdAt: timestamp("created_at", { mode: "date", withTimezone: true, precision: 3 }).notNull().defaultNow(), + }, + (t) => [index("email_change_tokens_user_idx").on(t.userId), uniqueIndex("email_change_tokens_hash_uidx").on(t.tokenHash)], +); + +export type EmailChangeToken = typeof emailChangeTokens.$inferSelect; + export type User = typeof users.$inferSelect; export type Credential = typeof credentials.$inferSelect; export type Identity = typeof identities.$inferSelect; diff --git a/src/lib/server/db/schema.sqlite.ts b/src/lib/server/db/schema.sqlite.ts index aa8290a..752fe5e 100644 --- a/src/lib/server/db/schema.sqlite.ts +++ b/src/lib/server/db/schema.sqlite.ts @@ -38,6 +38,10 @@ export const users = sqliteTable( username: text("username"), email: text("email").notNull(), emailVerifiedAt: integer("email_verified_at", { mode: "timestamp_ms" }), + // F3: 이메일 변경 대기 상태. 새 주소 확인(email_change_tokens) 전까지 여기 보관하고, + // 확인 완료 시 email 로 승격 후 NULL 로 클리어한다. requestedAt 은 대기 시작 시각. + pendingEmail: text("pending_email"), + pendingEmailRequestedAt: integer("pending_email_requested_at", { mode: "timestamp_ms" }), displayName: text("display_name"), role: text("role", { enum: ["admin", "user"] }) .notNull() @@ -71,7 +75,17 @@ export const users = sqliteTable( .notNull() .default(sql`(unixepoch() * 1000)`), }, - (t) => [uniqueIndex("users_tenant_email_uidx").on(t.tenantId, t.email), uniqueIndex("users_tenant_username_uidx").on(t.tenantId, t.username), index("users_tenant_idx").on(t.tenantId)], + (t) => [ + uniqueIndex("users_tenant_email_uidx").on(t.tenantId, t.email), + uniqueIndex("users_tenant_username_uidx").on(t.tenantId, t.username), + index("users_tenant_idx").on(t.tenantId), + // GC(하드삭제) 조회 지원 — status='deletion_pending' & deletionScheduledAt 경과분만 스캔한다. + // 부분 인덱스(WHERE status='deletion_pending')로 삭제 예정 계정만 색인해 공간을 아낀다. + // (mysql 은 부분 인덱스 미지원 → users_deletion_gc_idx 복합 인덱스로 대체; parity 예외 등재.) + index("users_deletion_pending_idx") + .on(t.deletionScheduledAt) + .where(sql`status = 'deletion_pending'`), + ], ); /** @@ -247,6 +261,9 @@ export const oidcClients = sqliteTable( idTokenSignedResponseAlg: text("id_token_signed_response_alg").notNull().default("RS256"), jwksUri: text("jwks_uri"), jwks: text("jwks"), + // organization scope 클레임의 클라이언트별 노출 토글(JSON). null=미설정=전량 노출(하위호환). + // 예: {"department":true,"team":true,"position":false,"jobTitle":true} + organizationClaimConfig: text("organization_claim_config"), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), createdAt: integer("created_at", { mode: "timestamp_ms" }) .notNull() @@ -940,6 +957,33 @@ export const inviteTokens = sqliteTable( export type InviteToken = typeof inviteTokens.$inferSelect; +// ---------- Email change ---------- +// F3: 프로필 이메일 변경 확인 토큰. email_verification_tokens 와 분리한다 — 변경 대상 주소 +// (targetEmail)를 토큰에 바인딩해야 하고(확인 링크가 다른 주소로 재사용되지 않도록), 확인 +// 라우트/시맨틱도 다르기 때문이다. SHA-256 해시 저장, TTL 24시간, 1회용(usedAt). +export const emailChangeTokens = sqliteTable( + "email_change_tokens", + { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + tokenHash: text("token_hash").notNull(), + // 변경하려는 새 이메일 주소(토큰에 바인딩). 확인 시 이 값으로 users.email 을 교체한다. + targetEmail: text("target_email").notNull(), + expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), + usedAt: integer("used_at", { mode: "timestamp_ms" }), + createdAt: integer("created_at", { mode: "timestamp_ms" }) + .notNull() + .default(sql`(unixepoch() * 1000)`), + }, + (t) => [index("email_change_tokens_user_idx").on(t.userId), uniqueIndex("email_change_tokens_hash_uidx").on(t.tokenHash)], +); + +export type EmailChangeToken = typeof emailChangeTokens.$inferSelect; + export type User = typeof users.$inferSelect; export type Credential = typeof credentials.$inferSelect; export type Identity = typeof identities.$inferSelect; From 4db21b297e96d464cd26eb581b19a050462c5d01 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 17:38:21 +0900 Subject: [PATCH 17/26] =?UTF-8?q?chore(i18n):=20=ED=9B=84=EC=86=8D=203?= =?UTF-8?q?=EC=B0=A8=20=EB=9D=BC=EC=9A=B4=EB=93=9C=20=ED=82=A4=20=EC=9D=BC?= =?UTF-8?q?=EA=B4=84=20=EB=B0=98=EC=98=81=20(ko/en=20=EB=8C=80=EC=B9=AD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - email.*: 트랜잭션 메일(find-id/비밀번호 재설정/이메일 인증/초대) locale 인지용 제목·본문·버튼·푸터 키 - security_alert.*: 세션 철회(session_revoked/sessions_revoked_all)·이메일 변경(email_change_requested)·백업코드(backup_codes_low/depleted) 알림 키 - profile.*: 이메일 변경 UI + 생년월일 형식 에러, sessions.*: 세션 지정/조회 에러, accept_invite.credential_label, skins.placeholder_* 3종 - 다수 페이즈(P12~P17)가 공유하는 키라 JSON hunk 분리 대신 일괄 반영(직전 라운드 c1fa642 관례). 코드 참조는 각 페이즈 커밋에서. Co-Authored-By: Claude Fable 5 --- src/lib/i18n/en.json | 113 +++++++++++++++++++++++++++++++++++++++++-- src/lib/i18n/ko.json | 113 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 218 insertions(+), 8 deletions(-) diff --git a/src/lib/i18n/en.json b/src/lib/i18n/en.json index b7b582c..9b2511c 100644 --- a/src/lib/i18n/en.json +++ b/src/lib/i18n/en.json @@ -35,7 +35,8 @@ "change": "Change", "saving": "Saved.", "search": "Search", - "next_page": "Next page →" + "next_page": "Next page →", + "processing": "Processing..." }, "login": { "username": "Username", @@ -202,7 +203,9 @@ "revoke_others_confirm": "Sign out every session except this device?", "empty": "No active sessions.", "revoked_notice": "The selected session was signed out.", - "revoked_others_notice": "Signed out of all other devices." + "revoked_others_notice": "Signed out of all other devices.", + "err_select_session": "Please select a session.", + "err_not_found": "Session not found." }, "danger_zone": { "title": "Delete account", @@ -297,7 +300,30 @@ "email_unverified_title": "Your email is not verified yet.", "email_unverified_desc": "We sent a verification email to {{email}}. Please check your inbox.", "email_resend": "Resend verification email", - "email_resend_sent": "Verification email resent. Please check your inbox shortly." + "email_resend_sent": "Verification email resent. Please check your inbox shortly.", + "err_birthdate_format": "The date of birth format is invalid. (YYYY-MM-DD)", + "email_change_title": "Change Email Address", + "email_change_desc": "We will send a confirmation email to the new address. The change completes only after you click the confirmation link.", + "email_change_new_label": "New Email Address", + "email_change_password_label": "Current Password", + "email_change_submit": "Send confirmation email", + "email_change_sent": "A confirmation email was sent to {{email}}. Click the link to complete the change.", + "email_change_pending": "A change to {{email}} is pending confirmation. Please check your inbox.", + "err_email_change_password": "Your current password is incorrect.", + "err_email_change_format": "The email address format is invalid.", + "err_email_change_same": "That is the same as your current email address.", + "err_email_change_taken": "That email address is already in use.", + "err_email_change_no_password": "This account has no password set, so the email address cannot be changed." + }, + "confirm_email_change": { + "title": "Confirm Email Change", + "subtitle": "Please complete the change to your new email address.", + "confirm_prompt": "Click the button below to complete the change to your email address.", + "submit": "Confirm email change", + "success": "Your email address has been changed.", + "invalid_link": "This link has expired or is invalid.", + "go_login": "Go to login", + "go_profile": "Go to profile" }, "audit": { "title": "Audit Log", @@ -533,6 +559,20 @@ "regenerate_secret": "Regenerate Secret", "regenerate_confirm": "Regenerate the secret? The existing secret will be invalidated immediately.", "delete_confirm": "Delete this client?", + "org_claims": { + "title": "Organization claim exposure", + "desc": "Choose which fields the organization scope exposes for this client. Applied identically to both the id_token and the userinfo response.", + "saved": "Organization claim settings saved.", + "all_note": "If every field is enabled, the setting is stored as unconfigured (all fields exposed).", + "field_department": "Department (department)", + "field_department_desc": "The list of departments the user belongs to, including each department's code, primary flag, job title, and position.", + "field_team": "Team (team)", + "field_team_desc": "The list of teams the user belongs to, including each team's code, parent department, primary flag, and job title.", + "field_position": "Position (position)", + "field_position_desc": "The position name of the user's primary department.", + "field_jobTitle": "Job title (job_title)", + "field_jobTitle_desc": "The job title of the user's primary department." + }, "errors": { "rate_limited": "Too many requests. Please try again shortly.", "rate_limited_short": "Too many requests.", @@ -696,12 +736,49 @@ "invalid_link": "This link has expired or is invalid.", "go_login": "Go to login", "err_password_short": "Password must be at least 8 characters.", - "err_password_mismatch": "Passwords do not match." + "err_password_mismatch": "Passwords do not match.", + "credential_label": "Password" }, "errors": { "rate_limit": "Too many requests. Please try again in {{minutes}} minute(s).", "db_not_ready": "The D1 binding \"DB\" is not ready. Please run in a Wrangler preview/dev environment." }, + "email": { + "footer": "If you did not request this, please ignore this email.", + "find_id": { + "subject": "Your username", + "title": "Username lookup", + "intro": "Here is the username you requested." + }, + "password_reset": { + "subject": "Reset your password", + "title": "Reset password", + "body": "Click the button below to reset your password. The link is valid for 1 hour.", + "button": "Reset password", + "text": "Use the link below to reset your password. The link is valid for 1 hour." + }, + "verify": { + "subject": "Verify your email", + "title": "Email verification", + "body": "Click the button below to verify your email address. The link is valid for 24 hours.", + "button": "Verify email", + "text": "Use the link below to verify your email address. The link is valid for 24 hours." + }, + "email_change": { + "subject": "Confirm your email change", + "title": "Confirm email change", + "body": "Click the button below to confirm the change to your new email address. The link is valid for 24 hours.", + "button": "Confirm email change", + "text": "Use the link below to confirm the change to your new email address. The link is valid for 24 hours." + }, + "invite": { + "subject": "You have been invited", + "title": "Account invitation", + "body": "You have been invited to an account. Click the button below to set your password and finish signing up. The link is valid for 72 hours.", + "button": "Accept invitation", + "text": "You have been invited to an account. Use the link below to set your password and finish signing up. The link is valid for 72 hours." + } + }, "security_alert": { "when": "Occurred at: {{time}}", "footer": "If this wasn't you, change your password immediately and contact your administrator.", @@ -740,6 +817,16 @@ "heading": "Backup codes were regenerated", "body": "New two-factor backup codes were generated for your account. Your previous backup codes no longer work." }, + "backup_codes_low": { + "subject": "[Security Alert] Your backup codes are running low", + "heading": "Your backup codes are running low", + "body": "A backup code was just used to sign in, and only a few unused backup codes remain. Please regenerate your backup codes soon so you don't get locked out." + }, + "backup_codes_depleted": { + "subject": "[Security Alert] Your backup codes are used up", + "heading": "Your backup codes are used up", + "body": "The last backup code on your account was just used to sign in. You have no unused backup codes left. Please regenerate your backup codes now to keep a recovery option available." + }, "passkey_added": { "subject": "[Security Alert] A new passkey was registered", "heading": "A new passkey was registered", @@ -754,6 +841,21 @@ "subject": "[Security Alert] Account deletion requested", "heading": "Account deletion requested", "body": "Your account deletion was received and moved to a pending-deletion state. You can recover it by signing in again within the 30-day grace period. If you did not request this, sign in immediately to recover and change your password." + }, + "email_change_requested": { + "subject": "[Security Alert] An email address change was requested", + "heading": "An email address change was requested", + "body": "A request to change your account's email address was received. The change completes only after the confirmation link sent to the new address is clicked. If you did not request this, change your password immediately and contact your administrator." + }, + "session_revoked": { + "subject": "[Security Alert] A session was signed out", + "heading": "A session was signed out", + "body": "One of your account's active sessions was just signed out (revoked)." + }, + "sessions_revoked_all": { + "subject": "[Security Alert] All other sessions were signed out", + "heading": "All other sessions were signed out", + "body": "All active sessions on your account except the current one were just signed out (revoked)." } }, "skins": { @@ -789,6 +891,9 @@ "placeholder_form_action": "Form action (POSTs to the current URL if empty)", "placeholder_redirect_to": "Redirect target URL after login", "placeholder_skin_hint": "Skin hint value (used in a hidden input)", + "placeholder_registered": "\"1\" right after signup completes, empty otherwise (used to show a post-signup notice on the login skin)", + "placeholder_password_reset": "\"1\" right after a password reset completes, empty otherwise (used to show a reset-complete notice on the login skin)", + "placeholder_flash_msg": "Server-filled flash message (e.g. an error). Already HTML-escaped; empty when there is nothing to show", "guide_title": "Custom Skin Development Guide", "guide_subtitle": "Fetch HTML from an external URL to fully customize the login and sign-up screens.", "guide_overview_title": "Overview", diff --git a/src/lib/i18n/ko.json b/src/lib/i18n/ko.json index af21c15..739e50a 100644 --- a/src/lib/i18n/ko.json +++ b/src/lib/i18n/ko.json @@ -35,7 +35,8 @@ "change": "변경", "saving": "저장되었습니다.", "search": "검색", - "next_page": "다음 페이지 →" + "next_page": "다음 페이지 →", + "processing": "처리 중..." }, "login": { "username": "아이디", @@ -202,7 +203,9 @@ "revoke_others_confirm": "현재 기기를 제외한 모든 세션을 로그아웃할까요?", "empty": "활성 세션이 없습니다.", "revoked_notice": "선택한 세션을 로그아웃했습니다.", - "revoked_others_notice": "다른 모든 기기에서 로그아웃했습니다." + "revoked_others_notice": "다른 모든 기기에서 로그아웃했습니다.", + "err_select_session": "세션을 지정해 주세요.", + "err_not_found": "세션을 찾을 수 없습니다." }, "danger_zone": { "title": "계정 탈퇴", @@ -297,7 +300,30 @@ "email_unverified_title": "이메일이 아직 인증되지 않았습니다.", "email_unverified_desc": "{{email}} 주소로 인증 메일을 보냈습니다. 받은 편지함을 확인해 주세요.", "email_resend": "인증 메일 재발송", - "email_resend_sent": "인증 메일을 다시 보냈습니다. 잠시 후 받은 편지함을 확인해 주세요." + "email_resend_sent": "인증 메일을 다시 보냈습니다. 잠시 후 받은 편지함을 확인해 주세요.", + "err_birthdate_format": "생년월일 형식이 올바르지 않습니다. (YYYY-MM-DD)", + "email_change_title": "이메일 주소 변경", + "email_change_desc": "새 이메일 주소로 확인 메일을 보냅니다. 확인 링크를 클릭해야 변경이 완료됩니다.", + "email_change_new_label": "새 이메일 주소", + "email_change_password_label": "현재 비밀번호", + "email_change_submit": "확인 메일 보내기", + "email_change_sent": "{{email}} 주소로 확인 메일을 보냈습니다. 링크를 클릭하면 변경이 완료됩니다.", + "email_change_pending": "{{email}} 주소로의 변경 확인 대기 중입니다. 받은 편지함을 확인해 주세요.", + "err_email_change_password": "현재 비밀번호가 올바르지 않습니다.", + "err_email_change_format": "이메일 형식이 올바르지 않습니다.", + "err_email_change_same": "현재 이메일과 동일한 주소입니다.", + "err_email_change_taken": "이미 사용 중인 이메일 주소입니다.", + "err_email_change_no_password": "이 계정은 비밀번호가 설정되어 있지 않아 이메일을 변경할 수 없습니다." + }, + "confirm_email_change": { + "title": "이메일 주소 변경 확인", + "subtitle": "새 이메일 주소로의 변경을 완료해 주세요.", + "confirm_prompt": "아래 버튼을 클릭하면 이메일 주소 변경이 완료됩니다.", + "submit": "이메일 변경 확인", + "success": "이메일 주소가 변경되었습니다.", + "invalid_link": "링크가 만료되었거나 유효하지 않습니다.", + "go_login": "로그인하러 가기", + "go_profile": "프로필로 가기" }, "audit": { "title": "감사 로그", @@ -533,6 +559,20 @@ "regenerate_secret": "시크릿 재생성", "regenerate_confirm": "시크릿을 재생성하시겠습니까? 기존 시크릿은 즉시 무효화됩니다.", "delete_confirm": "클라이언트를 삭제하시겠습니까?", + "org_claims": { + "title": "조직 클레임 노출 설정", + "desc": "이 클라이언트의 organization scope 가 노출하는 필드를 선택합니다. id_token 과 userinfo 응답에 동일하게 적용됩니다.", + "saved": "조직 클레임 설정을 저장했습니다.", + "all_note": "모든 필드를 켜면 미설정(전량 노출) 상태로 저장됩니다.", + "field_department": "부서 (department)", + "field_department_desc": "사용자가 소속된 부서 목록. 각 부서의 코드·주소속 여부·직책·직급을 포함합니다.", + "field_team": "팀 (team)", + "field_team_desc": "사용자가 소속된 팀 목록. 각 팀의 코드·상위 부서·주소속 여부·직책을 포함합니다.", + "field_position": "직급 (position)", + "field_position_desc": "사용자 주소속 부서의 직급명.", + "field_jobTitle": "직책 (job_title)", + "field_jobTitle_desc": "사용자 주소속 부서의 직책." + }, "errors": { "rate_limited": "요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.", "rate_limited_short": "요청이 너무 많습니다.", @@ -696,12 +736,49 @@ "invalid_link": "링크가 만료되었거나 유효하지 않습니다.", "go_login": "로그인하러 가기", "err_password_short": "비밀번호는 8자 이상이어야 합니다.", - "err_password_mismatch": "비밀번호가 일치하지 않습니다." + "err_password_mismatch": "비밀번호가 일치하지 않습니다.", + "credential_label": "비밀번호" }, "errors": { "rate_limit": "요청이 너무 많습니다. {{minutes}}분 후 다시 시도해 주세요.", "db_not_ready": "D1 binding \"DB\" 가 준비되지 않았습니다. Wrangler preview/dev 환경에서 실행해 주세요." }, + "email": { + "footer": "본인이 요청하지 않았다면 이 이메일을 무시해 주세요.", + "find_id": { + "subject": "아이디 안내", + "title": "아이디 확인", + "intro": "요청하신 아이디 정보입니다." + }, + "password_reset": { + "subject": "비밀번호 재설정 안내", + "title": "비밀번호 재설정", + "body": "아래 버튼을 클릭하여 비밀번호를 재설정하세요. 링크는 1시간 동안 유효합니다.", + "button": "비밀번호 재설정", + "text": "아래 링크에서 비밀번호를 재설정하세요. 링크는 1시간 동안 유효합니다." + }, + "verify": { + "subject": "이메일 인증 안내", + "title": "이메일 인증", + "body": "아래 버튼을 클릭하여 이메일 주소를 인증하세요. 링크는 24시간 동안 유효합니다.", + "button": "이메일 인증", + "text": "아래 링크에서 이메일 주소를 인증하세요. 링크는 24시간 동안 유효합니다." + }, + "email_change": { + "subject": "이메일 주소 변경 확인", + "title": "이메일 주소 변경 확인", + "body": "아래 버튼을 클릭하여 새 이메일 주소로의 변경을 확인하세요. 링크는 24시간 동안 유효합니다.", + "button": "이메일 변경 확인", + "text": "아래 링크에서 새 이메일 주소로의 변경을 확인하세요. 링크는 24시간 동안 유효합니다." + }, + "invite": { + "subject": "계정 초대 안내", + "title": "계정 초대", + "body": "계정에 초대되었습니다. 아래 버튼을 클릭하여 비밀번호를 설정하고 가입을 완료하세요. 링크는 72시간 동안 유효합니다.", + "button": "초대 수락하기", + "text": "계정에 초대되었습니다. 아래 링크에서 비밀번호를 설정하고 가입을 완료하세요. 링크는 72시간 동안 유효합니다." + } + }, "security_alert": { "when": "발생 시각: {{time}}", "footer": "본인이 한 작업이 아니라면 즉시 비밀번호를 변경하고 관리자에게 문의해 주세요.", @@ -740,6 +817,16 @@ "heading": "백업 코드가 재생성되었습니다", "body": "계정의 2단계 인증 백업 코드가 새로 생성되었습니다. 기존 백업 코드는 더 이상 사용할 수 없습니다." }, + "backup_codes_low": { + "subject": "[보안 알림] 백업 코드가 얼마 남지 않았습니다", + "heading": "백업 코드가 얼마 남지 않았습니다", + "body": "방금 백업 코드로 로그인했으며, 사용하지 않은 백업 코드가 몇 개 남지 않았습니다. 로그인이 불가능해지지 않도록 곧 백업 코드를 재생성해 주세요." + }, + "backup_codes_depleted": { + "subject": "[보안 알림] 백업 코드를 모두 사용했습니다", + "heading": "백업 코드를 모두 사용했습니다", + "body": "방금 계정의 마지막 백업 코드로 로그인했습니다. 사용할 수 있는 백업 코드가 더 이상 없습니다. 복구 수단을 유지하려면 지금 백업 코드를 재생성해 주세요." + }, "passkey_added": { "subject": "[보안 알림] 새 패스키가 등록되었습니다", "heading": "새 패스키가 등록되었습니다", @@ -754,6 +841,21 @@ "subject": "[보안 알림] 계정 탈퇴가 접수되었습니다", "heading": "계정 탈퇴가 접수되었습니다", "body": "계정 탈퇴가 접수되어 삭제 예정 상태로 전환되었습니다. 유예기간(30일) 내에 다시 로그인하면 계정을 복구할 수 있습니다. 본인이 요청하지 않았다면 즉시 로그인해 복구하고 비밀번호를 변경해 주세요." + }, + "email_change_requested": { + "subject": "[보안 알림] 이메일 주소 변경이 요청되었습니다", + "heading": "이메일 주소 변경이 요청되었습니다", + "body": "계정의 이메일 주소를 변경하려는 요청이 접수되었습니다. 새 주소로 보낸 확인 링크를 클릭해야 변경이 완료됩니다. 본인이 요청하지 않았다면 즉시 비밀번호를 변경하고 관리자에게 문의해 주세요." + }, + "session_revoked": { + "subject": "[보안 알림] 세션이 로그아웃되었습니다", + "heading": "세션이 로그아웃되었습니다", + "body": "계정의 활성 세션 하나가 방금 로그아웃(철회)되었습니다." + }, + "sessions_revoked_all": { + "subject": "[보안 알림] 다른 모든 세션이 로그아웃되었습니다", + "heading": "다른 모든 세션이 로그아웃되었습니다", + "body": "현재 세션을 제외한 계정의 모든 활성 세션이 방금 로그아웃(철회)되었습니다." } }, "skins": { @@ -789,6 +891,9 @@ "placeholder_form_action": "폼 action (비어 있으면 현재 URL로 POST)", "placeholder_redirect_to": "로그인 후 리다이렉트 대상 URL", "placeholder_skin_hint": "스킨 힌트 값 (hidden input에 활용)", + "placeholder_registered": "회원가입 완료 직후 \"1\", 그 외에는 빈 값 (로그인 스킨에서 가입 완료 안내 노출에 활용)", + "placeholder_password_reset": "비밀번호 재설정 완료 직후 \"1\", 그 외에는 빈 값 (로그인 스킨에서 재설정 완료 안내 노출에 활용)", + "placeholder_flash_msg": "서버가 채우는 플래시 메시지(예: 오류). 이미 HTML 이스케이프됨. 표시할 내용이 없으면 빈 값", "guide_title": "커스텀 스킨 개발 가이드", "guide_subtitle": "외부 URL에서 HTML을 가져와 로그인·회원가입 화면을 완전히 커스터마이즈할 수 있습니다.", "guide_overview_title": "개요", From 57072a18fcb5a3b128f7c94ca809e46a5ce03c38 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 17:39:06 +0900 Subject: [PATCH 18/26] =?UTF-8?q?fix:=20=EC=9E=94=EC=97=AC=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=C2=B7=EB=B3=B4=EC=95=88=C2=B7=EC=9A=B4=EC=98=81=20?= =?UTF-8?q?=EB=A7=88=EA=B0=90=20(P12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gc: invite_tokens·email_change_tokens purge 추가(만료 또는 소진분 정리, 미만료·미소진 보존) + users 하드삭제를 배치(id 조회→IN 삭제 루프)로 전환해 대량 cascade 락 방지 + GC 결과 구조화(JSON) 로깅 - guards: assertNotLastAdmin 이 "로그인 가능한(credential/identities 보유)" admin 만 카운트 — 미수락 초대 admin 이 마지막 관리자 삭제 보호를 잘못 완화하는 문제 차단 - accept-invite: lookupToken 조인에 status='active' 강제(비활성/삭제예정 계정에 credential 심기 차단), credential label i18n - health: 경량 SELECT 1 readiness 체크 — DB 불가 시 503 - gc·schema-parity 테스트 보강 - gc.ts 는 email_change purge(P13)·구조화 로깅(P17)이 같은 파일에 얽혀 한 커밋으로 통합. 배치 삭제는 방언 이식성 위해 DELETE…LIMIT 대신 2단계 루프. Co-Authored-By: Claude Fable 5 --- src/lib/server/auth/guards.ts | 25 ++++++- src/lib/server/db/gc.ts | 73 ++++++++++++++++++- .../(auth)/accept-invite/+page.server.ts | 6 +- src/routes/api/health/+server.ts | 35 +++++---- test/unit/gc.test.ts | 61 ++++++++++++++-- test/unit/schema-parity.test.ts | 16 ++++ 6 files changed, 185 insertions(+), 31 deletions(-) diff --git a/src/lib/server/auth/guards.ts b/src/lib/server/auth/guards.ts index 0c41507..7391c91 100644 --- a/src/lib/server/auth/guards.ts +++ b/src/lib/server/auth/guards.ts @@ -1,7 +1,7 @@ import { error, fail } from "@sveltejs/kit"; -import { and, eq, ne } from "drizzle-orm"; +import { and, eq, ne, or, exists, sql } from "drizzle-orm"; import type { DB } from "$lib/server/db"; -import { users } from "$lib/server/db/schema"; +import { users, credentials, identities } from "$lib/server/db/schema"; export function requireDbContext(locals: App.Locals) { if (!locals.db || !locals.tenant) { @@ -48,10 +48,29 @@ export async function assertNotLastAdmin(db: DB, tenantId: string, userIdToBeCha if (!target) return null; if (target.role !== "admin" || target.status !== "active") return null; + // "다른 활성 admin" 은 실제로 **로그인 가능한** 계정만 센다. status=active 지만 아직 + // credential/identity 가 없는 계정(예: 미수락 초대 admin)은 로그인 자체가 불가능하므로, + // 이런 계정이 last-admin 보호를 완화(다른 관리자 존재로 오인)하지 못하게 한다. + // 로그인 가능 판정: password/passkey 등 credential 이 있거나, 연합(identities) 계정이면 사용 가능. + // (LDAP/OIDC/SAML 은 identities row, password/webauthn 은 credentials row 를 가진다.) + const usableAdmin = or( + exists( + db + .select({ one: sql`1` }) + .from(credentials) + .where(eq(credentials.userId, users.id)), + ), + exists( + db + .select({ one: sql`1` }) + .from(identities) + .where(eq(identities.userId, users.id)), + ), + ); const otherAdmins = await db .select({ id: users.id }) .from(users) - .where(and(eq(users.tenantId, tenantId), eq(users.role, "admin"), eq(users.status, "active"), ne(users.id, userIdToBeChanged))) + .where(and(eq(users.tenantId, tenantId), eq(users.role, "admin"), eq(users.status, "active"), ne(users.id, userIdToBeChanged), usableAdmin)) .limit(1); if (otherAdmins.length === 0) { diff --git a/src/lib/server/db/gc.ts b/src/lib/server/db/gc.ts index ff3587c..1721a82 100644 --- a/src/lib/server/db/gc.ts +++ b/src/lib/server/db/gc.ts @@ -21,9 +21,9 @@ * GC 실패는 요청 처리에 절대 영향을 주지 않는다(전부 try/catch + waitUntil 격리). */ -import { and, eq, lt, or } from "drizzle-orm"; +import { and, eq, lt, or, isNotNull, inArray } from "drizzle-orm"; import { getDb, DB_DIALECT, type DB } from "./index"; -import { sessions, oidcGrants, passwordResetTokens, emailVerificationTokens, samlSloStates, samlAuthnRequestIds, samlSessions, users } from "./schema"; +import { sessions, oidcGrants, passwordResetTokens, emailVerificationTokens, emailChangeTokens, samlSloStates, samlAuthnRequestIds, samlSessions, users, inviteTokens } from "./schema"; import { purgeExpiredChallenges } from "$lib/server/auth/webauthn"; import { purgeExpiredRefreshTokens, REFRESH_TOKEN_TTL_MS } from "$lib/server/oidc/refresh"; import { purgeExpiredRateLimits } from "$lib/server/ratelimit"; @@ -72,6 +72,14 @@ const NODE_GC_INTERVAL_MS = 60 * 60 * 1000; /** Workers 확률적 GC 발사 비율(요청의 ~1%). */ const WORKERS_GC_SAMPLE_RATE = 0.01; +/** + * users 하드삭제 배치 크기. 대량의 deletion_pending 계정이 한 번에 삭제되면 FK cascade + * (credentials/sessions/identities/…)로 인한 광범위한 락이 걸릴 수 있어, id 를 배치 단위로 + * 조회해 나눠 삭제한다. DELETE … LIMIT 는 방언별(sqlite/pg/mysql) 이식성이 없어 "id 조회 → + * IN(...) 삭제" 2단계 루프로 이식성 있게 구현한다. + */ +const USERS_GC_BATCH_SIZE = 100; + // ── 결과 타입 ──────────────────────────────────────────────────────────────────── export interface GcTableResult { @@ -105,6 +113,39 @@ function extractAffected(res: unknown): number | null { return null; } +/** + * users 하드삭제를 배치로 수행한다(대량 cascade 락 방지). deletion_pending & 유예 경과 계정의 + * id 를 USERS_GC_BATCH_SIZE 만큼 조회 → IN(...) 삭제하는 루프. 마지막 배치(조회 수 < 배치 크기) + * 이후 종료한다. 방언 이식성을 위해 DELETE … LIMIT 대신 2단계(select→delete)로 구현한다. + * 자체 try/catch 로 에러를 격리하고 결과를 tables 에 push 한다(다른 테이블 GC 에 영향 없음). + */ +async function runUsersBatchDelete(db: DB, now: Date, tables: GcTableResult[]): Promise { + try { + let total = 0; + for (;;) { + const rows = await db + .select({ id: users.id }) + .from(users) + .where(and(eq(users.status, "deletion_pending"), lt(users.deletionScheduledAt, now))) + .limit(USERS_GC_BATCH_SIZE); + if (rows.length === 0) break; + await db.delete(users).where( + inArray( + users.id, + rows.map((r) => r.id), + ), + ); + total += rows.length; + if (rows.length < USERS_GC_BATCH_SIZE) break; + } + tables.push({ table: "users", deleted: total, ok: true }); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + console.error(`[gc] delete users 실패:`, msg); + tables.push({ table: "users", deleted: null, ok: false, error: msg }); + } +} + // ── 통합 GC ────────────────────────────────────────────────────────────────────── /** @@ -162,6 +203,16 @@ export async function runExpiredDataGc(db: DB): Promise { // email_verification_tokens: expiresAt 경과 시 삭제. 사용됨·미사용 모두 만료 후엔 무효. await runDelete("email_verification_tokens", () => db.delete(emailVerificationTokens).where(lt(emailVerificationTokens.expiresAt, now))); + // email_change_tokens: 만료(expiresAt 경과) 또는 소진(usedAt 설정)된 이메일 변경 토큰 삭제. + // invite_tokens 와 동일 시맨틱 — 소진분도 재사용 불가하므로 함께 정리(무한 성장 차단). + // 미만료·미소진 토큰(유효한 변경 대기)은 두 조건 모두 거짓이라 보존된다. + await runDelete("email_change_tokens", () => db.delete(emailChangeTokens).where(or(lt(emailChangeTokens.expiresAt, now), isNotNull(emailChangeTokens.usedAt)))); + + // invite_tokens: 만료(expiresAt 경과) 또는 소진(usedAt 설정)된 초대 토큰 삭제. 다른 토큰 + // 테이블과 동일 시맨틱이되, 소진분도 재사용 불가하므로 함께 정리한다(무한 성장 차단). + // 미만료·미소진 토큰(유효한 초대 대기)은 두 조건 모두 거짓이라 보존된다. + await runDelete("invite_tokens", () => db.delete(inviteTokens).where(or(lt(inviteTokens.expiresAt, now), isNotNull(inviteTokens.usedAt)))); + // saml_slo_states: SLO 체인 상태. 자체 expiresAt 경과 시 삭제(만료된 체인은 죽은 상태). await runDelete("saml_slo_states", () => db.delete(samlSloStates).where(lt(samlSloStates.expiresAt, now))); @@ -185,13 +236,27 @@ export async function runExpiredDataGc(db: DB): Promise { // **보수적 조건**: (a) status 가 정확히 deletion_pending 이고 (b) deletionScheduledAt < now 인 // 행만 대상이다. deletionScheduledAt 이 NULL 인 활성/일반 계정은 `<` 비교에서 참이 되지 않아 // 절대 매칭되지 않으며(활성 계정 오삭제 불가), 유예 미경과 계정도 삭제되지 않는다. - await runDelete("users", () => db.delete(users).where(and(eq(users.status, "deletion_pending"), lt(users.deletionScheduledAt, now)))); + // 대량 cascade 락을 피하려고 배치(USERS_GC_BATCH_SIZE)로 id 를 조회해 나눠 삭제한다. + await runUsersBatchDelete(db, now, tables); const result: GcResult = { startedAt, durationMs: Date.now() - startedAt, tables }; const totalDeleted = tables.reduce((sum, t) => sum + (t.deleted ?? 0), 0); const failed = tables.filter((t) => !t.ok).map((t) => t.table); - console.log(`[gc] 완료 ${result.durationMs}ms — 삭제 ${totalDeleted}+ 건` + (failed.length ? `, 실패 테이블: ${failed.join(", ")}` : ""), tables); + // 구조화(JSON) 로깅 — 로그 수집기에서 파싱 가능한 단일 라인으로 GC 결과를 남긴다. + // 과설계(전면 로거/메트릭 파이프라인)는 범위 밖 — 결과 요약 + 테이블별 상세만 직렬화한다. + // deleted 는 방언별로 산출 불가할 수 있어 null 이면 "+" 의미(최소 건수)로 totalDeleted 에 0 처리. + console.log( + JSON.stringify({ + event: "gc_complete", + startedAt: new Date(result.startedAt).toISOString(), + durationMs: result.durationMs, + totalDeleted, + ok: failed.length === 0, + failedTables: failed, + tables: result.tables, + }), + ); return result; } diff --git a/src/routes/(auth)/accept-invite/+page.server.ts b/src/routes/(auth)/accept-invite/+page.server.ts index 4c3fa4e..a268582 100644 --- a/src/routes/(auth)/accept-invite/+page.server.ts +++ b/src/routes/(auth)/accept-invite/+page.server.ts @@ -20,7 +20,9 @@ async function lookupToken(db: App.Locals["db"], tenantId: string, token: string .select({ tokenId: inviteTokens.id, userId: users.id, expiresAt: inviteTokens.expiresAt }) .from(inviteTokens) .innerJoin(users, eq(inviteTokens.userId, users.id)) - .where(and(eq(inviteTokens.tokenHash, tokenHash), isNull(inviteTokens.usedAt), eq(users.tenantId, tenantId))) + // status='active' 강제 — 비활성(disabled/locked)·삭제예정(deletion_pending) 계정에 credential 을 + // 심는 것을 차단한다. 소진 직전(action)에도 이 lookupToken 을 재호출하므로 TOCTOU 창을 최소화한다. + .where(and(eq(inviteTokens.tokenHash, tokenHash), isNull(inviteTokens.usedAt), eq(users.tenantId, tenantId), eq(users.status, "active"))) .limit(1); if (!record || record.expiresAt < now) return null; return record; @@ -74,7 +76,7 @@ export const actions: Actions = { userId: record.userId, type: PASSWORD_CREDENTIAL_TYPE, secret: hashedPw, - label: "비밀번호", + label: translate(locale, "accept_invite.credential_label"), }), (h) => h.update(users).set({ emailVerifiedAt: now, updatedAt: now }).where(eq(users.id, record.userId)), (h) => diff --git a/src/routes/api/health/+server.ts b/src/routes/api/health/+server.ts index f362707..600a106 100644 --- a/src/routes/api/health/+server.ts +++ b/src/routes/api/health/+server.ts @@ -1,22 +1,29 @@ import { json } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; +import { tenants } from "$lib/server/db/schema"; /** - * 헬스체크 엔드포인트 (liveness + 얕은 readiness). + * 헬스체크 엔드포인트 (liveness + readiness). * * hooks.server.ts 는 이 경로에서 auth baseline 조회를 건너뛴다(부하 최소화). - * DB 바인딩이 초기화됐는지만 얕게 보고한다. 200 = 프로세스 살아있음. + * + * - DB 바인딩이 없으면 즉시 503(unavailable). + * - 바인딩이 있으면 경량 쿼리(tenants LIMIT 1)로 실제 연결 가능 여부까지 확인한다. + * 쿼리 실패(DB 다운/네트워크 등) 시 503 을 반환해 로드밸런서/오케스트레이터가 + * 해당 인스턴스로 트래픽을 보내지 않게 한다(과거엔 DB 다운에도 항상 200 이었다). + * - 정상이면 200 { status: "ok", db: "ready" }. */ -export const GET: RequestHandler = ({ locals }) => { - const dbReady = Boolean(locals.db); - return json( - { - status: "ok", - db: dbReady ? "ready" : "unavailable", - }, - { - status: 200, - headers: { "Cache-Control": "no-store" }, - }, - ); +export const GET: RequestHandler = async ({ locals }) => { + if (!locals.db) { + return json({ status: "error", db: "unavailable" }, { status: 503, headers: { "Cache-Control": "no-store" } }); + } + + try { + // 경량 readiness 프로브 — 항상 존재하는 tenants 테이블을 한 행만 조회해 연결을 확인한다. + await locals.db.select({ id: tenants.id }).from(tenants).limit(1); + } catch { + return json({ status: "error", db: "unavailable" }, { status: 503, headers: { "Cache-Control": "no-store" } }); + } + + return json({ status: "ok", db: "ready" }, { status: 200, headers: { "Cache-Control": "no-store" } }); }; diff --git a/test/unit/gc.test.ts b/test/unit/gc.test.ts index d05bd14..4001c07 100644 --- a/test/unit/gc.test.ts +++ b/test/unit/gc.test.ts @@ -23,6 +23,7 @@ interface DeleteCapture { function makeDb(opts: { failFor?: string[] } = {}) { const deletes: DeleteCapture[] = []; + const selects: DeleteCapture[] = []; const db = { delete: (table: unknown) => { const name = getTableName(table as Parameters[0]); @@ -34,14 +35,35 @@ function makeDb(opts: { failFor?: string[] } = {}) { }, }; }, + // users 배치 하드삭제(runUsersBatchDelete)는 select().from(users).where(cond).limit(n) 로 + // id 를 조회한 뒤 delete(users).where(inArray(...)) 한다. select 를 캡처하고, users 는 + // 한 배치(배치 크기보다 작음)만 반환해 루프가 정확히 1회 만에 종료되게 한다. + select: () => ({ + from: (table: unknown) => { + const name = getTableName(table as Parameters[0]); + const builder = { + _where: undefined as SQL | undefined, + where(where: SQL) { + this._where = where; + return this; + }, + async limit() { + if (opts.failFor?.includes(name)) throw new Error(`boom:${name}`); + if (this._where) selects.push({ table: name, where: this._where }); + return name === "users" ? [{ id: "u1" }] : []; + }, + }; + return builder; + }, + }), } as unknown as DB; - return { db, deletes }; + return { db, deletes, selects }; } -/** 캡처된 delete 목록에서 테이블명으로 where 를 찾아 렌더링한다. */ -function whereFor(deletes: DeleteCapture[], table: string): { sql: string; params: unknown[] } { - const cap = deletes.find((d) => d.table === table); - if (!cap) throw new Error(`delete for ${table} 미실행`); +/** 캡처된 delete/select 목록에서 테이블명으로 where 를 찾아 렌더링한다. */ +function whereFor(captures: DeleteCapture[], table: string): { sql: string; params: unknown[] } { + const cap = captures.find((d) => d.table === table); + if (!cap) throw new Error(`capture for ${table} 미실행`); return render(cap.where); } @@ -64,6 +86,8 @@ const ALL_TABLES = [ "oidc_grants", "password_reset_tokens", "email_verification_tokens", + "email_change_tokens", + "invite_tokens", "saml_slo_states", "saml_authn_request_ids", "saml_sessions", @@ -147,13 +171,14 @@ describe("runExpiredDataGc — 보수적 만료 조건", () => { expect(c1).toBeGreaterThanOrEqual(start - SESSION_TTL_MS - 5_000); }); - it("users: status=deletion_pending 이고 deletionScheduledAt 경과분만 삭제한다(활성/미경과 보존)", async () => { + it("users: status=deletion_pending 이고 deletionScheduledAt 경과분만 (배치)조회·삭제한다(활성/미경과 보존)", async () => { const start = Date.now(); - const { db, deletes } = makeDb(); + const { db, deletes, selects } = makeDb(); await runExpiredDataGc(db); const end = Date.now(); - const { sql, params } = whereFor(deletes, "users"); + // 배치 하드삭제는 대상 id 를 select 로 먼저 조회한다 — 대상 조건(where)을 이 select 에서 검증. + const { sql, params } = whereFor(selects, "users"); // 두 조건(AND): status = 'deletion_pending' 그리고 deletion_scheduled_at < now. expect(sql).toContain('"users"."status" = ?'); expect(sql).toContain('"users"."deletion_scheduled_at" < ?'); @@ -167,6 +192,26 @@ describe("runExpiredDataGc — 보수적 만료 조건", () => { // GC 는 예정 시각 경과분만 지운다. expect(cutoff).toBeGreaterThanOrEqual(start - 5_000); expect(cutoff).toBeLessThanOrEqual(end); + + // 조회된 id 로 실제 삭제(inArray)가 배치 수행된다. + const delSql = whereFor(deletes, "users").sql; + expect(delSql).toContain('"users"."id" in'); + }); + + it("invite_tokens: 만료(expiresAt 경과) 또는 소진(usedAt 설정) 토큰을 삭제한다(유효 대기분 보존)", async () => { + const start = Date.now(); + const { db, deletes } = makeDb(); + await runExpiredDataGc(db); + const end = Date.now(); + + const { sql, params } = whereFor(deletes, "invite_tokens"); + // 두 분기(OR): 만료(expires_at <) 또는 소진(used_at is not null). + expect(sql).toContain('"invite_tokens"."expires_at" < ?'); + expect(sql).toContain('"invite_tokens"."used_at" is not null'); + expect(sql).toContain(" or "); + const cutoff = params[0] as number; + expect(cutoff).toBeGreaterThanOrEqual(start - 5_000); + expect(cutoff).toBeLessThanOrEqual(end); }); it("oidc_grants / password_reset_tokens / saml_slo_states: expiresAt 경과분만 삭제한다", async () => { diff --git a/test/unit/schema-parity.test.ts b/test/unit/schema-parity.test.ts index 45efc1b..71cc427 100644 --- a/test/unit/schema-parity.test.ts +++ b/test/unit/schema-parity.test.ts @@ -74,6 +74,22 @@ const INDEX_PARITY_EXCEPTIONS: IndexParityException[] = [ // mysql 은 애플리케이션 레벨 트랜잭션으로 동일 불변식을 보장한다(schema.mysql.ts 주석). reason: "MySQL 은 partial unique index 미지원 → 앱 레벨 트랜잭션으로 대체", }, + { + table: "users", + index: "users_deletion_pending_idx", + missingIn: ["mysql"], + // sqlite/pg 는 부분 인덱스(WHERE status='deletion_pending')로 삭제 예정 계정만 색인한다. + // MySQL 은 부분 인덱스를 지원하지 않아 아래 users_deletion_gc_idx(복합)로 대체한다. + reason: "MySQL 은 partial index 미지원 → users_deletion_gc_idx 복합 인덱스로 대체", + }, + { + table: "users", + index: "users_deletion_gc_idx", + missingIn: ["sqlite", "pg"], + // MySQL 전용 복합 인덱스(status, deletionScheduledAt). sqlite/pg 는 부분 인덱스 + // users_deletion_pending_idx 로 동일 GC 조회를 더 좁게 색인하므로 이 인덱스가 없다. + reason: "sqlite/pg 는 부분 인덱스 users_deletion_pending_idx 사용 → 복합 인덱스 불필요", + }, ]; // ── introspection 정규화 ────────────────────────────────────────────────────────── From 5bd3e422f107be83f6c5f4682bc6c6514989a91c Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 17:42:51 +0900 Subject: [PATCH 19/26] =?UTF-8?q?refactor(ratelimit):=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EC=86=8C=20=EC=B6=94=EC=83=81=ED=99=94=20=E2=80=94=20RateLimit?= =?UTF-8?q?Store=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20+=20Nod?= =?UTF-8?q?e=20in-memory=20(P15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ratelimit/store.ts 신설: RateLimitStore{increment/peek} 인터페이스 + DbRateLimitStore(4방언 upsert 캡슐화)·MemoryRateLimitStore(Map, 자체 evict)·resolveRateLimitStore(Workers=DB, Node=memory) - ratelimit/index.ts: checkRateLimit(store,…)·peekRateLimit(store,…) 로 시그니처 전환, 슬라이딩 윈도우 산식(evaluate)만 남기고 상태 연산은 store 위임. 알고리즘 byte-identical - hooks: 요청당 locals.rateLimitStore 해석, app.d.ts Locals 확장, guards.requireDbContext 가 store 반환 - 호출부 전량 이관(checkRateLimit(db)→store): login(중복 accountLockStatus 를 peekRateLimit 로 대체)·admin-login·signup·find-id·find-password·reset-password·verify-email·accept-invite·mfa·profile(재발송)·token(×2)·totp(×2)·webauthn(×3)·oidc(authorize/introspect/revoke)·saml-sso - .env.example: Node in-memory 단일 인스턴스 한계·Redis 확장 경로 문서화(A3) - breaking 시그니처라 24개 호출부·인프라를 한 커밋에 묶음. token 조직클레임(P17)·find*/signup/profile 메일locale(P13)·mfa 백업(P17) 등 동일 파일의 타 페이즈 hunk 는 후속 커밋으로 분리. Co-Authored-By: Claude Fable 5 --- .env.example | 9 + src/app.d.ts | 2 + src/hooks.server.ts | 4 + src/lib/server/auth/guards.ts | 4 +- src/lib/server/ratelimit/index.ts | 89 +++--- src/lib/server/ratelimit/store.ts | 166 ++++++++++ .../(auth)/accept-invite/+page.server.ts | 4 +- src/routes/(auth)/find-id/+page.server.ts | 4 +- .../(auth)/find-password/+page.server.ts | 4 +- src/routes/(auth)/login/+page.server.ts | 42 +-- src/routes/(auth)/mfa/+page.server.ts | 4 +- .../(auth)/reset-password/+page.server.ts | 4 +- src/routes/(auth)/signup/+page.server.ts | 4 +- .../(auth)/verify-email/+page.server.ts | 4 +- src/routes/account/profile/+page.server.ts | 4 +- src/routes/admin/login/+page.server.ts | 4 +- src/routes/api/totp/enroll/confirm/+server.ts | 4 +- src/routes/api/totp/verify/+server.ts | 4 +- .../webauthn/authenticate/options/+server.ts | 4 +- .../webauthn/authenticate/verify/+server.ts | 4 +- .../api/webauthn/register/verify/+server.ts | 4 +- src/routes/oidc/authorize/+server.ts | 4 +- src/routes/oidc/introspect/+server.ts | 4 +- src/routes/oidc/revoke/+server.ts | 4 +- src/routes/oidc/token/+server.ts | 6 +- src/routes/saml/sso/+server.ts | 4 +- test/unit/ratelimit.test.ts | 302 +++++++++++++----- 27 files changed, 490 insertions(+), 206 deletions(-) create mode 100644 src/lib/server/ratelimit/store.ts diff --git a/.env.example b/.env.example index c6593ee..17b7b77 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,15 @@ # - node: 순수 Node 서버 (adapter-node). platform 바인딩 없음 → pg/mysql 은 # DATABASE_URL 로 직결, 설정값은 process.env 로 읽는다. (D1 은 Workers 전용) # 예) BUILD_TARGET=node DB_DIALECT=postgres bun run build && node build +# +# 레이트 리밋 저장소(RateLimitStore)는 배포 타깃에 따라 자동 선택된다: +# - cloudflare(Workers): 요청 간 상태를 공유할 수 없는 isolate 특성상 DB(rate_limits 테이블) +# 기반 공유 저장소를 사용한다. +# - node(adapter-node) : 프로세스 내 in-memory 저장소를 사용한다(핫패스 DB write 없음). +# ⚠️ in-memory 는 "단일 프로세스" 가정이다. 같은 앱을 여러 인스턴스로 수평 확장하면 +# 각 인스턴스가 자기 카운터만 보므로 실질 한도가 인스턴스 수만큼 완화된다. +# 다중 인스턴스 Node 배포에서 엄격한 전역 한도가 필요하면 공유 저장소(Redis 등)로 +# RateLimitStore 를 교체해야 한다(인터페이스는 열려 있으나 이번 버전엔 미포함). BUILD_TARGET="cloudflare" # DB_DIALECT: d1(기본) | sqlite | postgres | mysql — 배포 단위로 하나만 사용 diff --git a/src/app.d.ts b/src/app.d.ts index c47d236..1d380f3 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -19,6 +19,8 @@ declare global { // interface Error {} interface Locals { db?: import("$lib/server/db").DB; + // 요청당 레이트 리밋 저장소. Workers=DB, Node=in-memory. db 와 함께 hooks 에서 세팅. + rateLimitStore?: import("$lib/server/ratelimit").RateLimitStore; tenant: import("$lib/server/db/schema").Tenant | null; session: import("$lib/server/db/schema").Session | null; user: import("$lib/server/db/schema").User | null; diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 52411c7..02bfed7 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -4,6 +4,7 @@ import { SESSION_COOKIE_NAME, SESSION_TOUCH_INTERVAL_MS } from "$lib/server/auth import { getRuntimeConfig } from "$lib/server/auth/runtime"; import { clearSessionCookie, getSessionContext, touchSession } from "$lib/server/auth/session"; import { getDb, DB_DIALECT } from "$lib/server/db"; +import { resolveRateLimitStore } from "$lib/server/ratelimit"; import { ensureNodeGcScheduler, maybeRunWorkersGc } from "$lib/server/db/gc"; import { LOCALE_COOKIE_NAME, resolveLocale } from "$lib/server/locale"; @@ -37,6 +38,7 @@ const PUBLIC_META = [/^\/\.well-known\//, /^\/oidc\/jwks/, /^\/saml\/metadata/]; export const handle: Handle = async ({ event, resolve }) => { event.locals.db = undefined; + event.locals.rateLimitStore = undefined; event.locals.tenant = null; event.locals.session = null; event.locals.user = null; @@ -99,6 +101,8 @@ export const handle: Handle = async ({ event, resolve }) => { const db = handle.db; disposeDb = handle.dispose; event.locals.db = db; + // 레이트 리밋 저장소: Workers=DB(요청당 db), Node=프로세스 전역 in-memory. + event.locals.rateLimitStore = resolveRateLimitStore(event.platform, db); // 만료 데이터 GC 실행 경로 (요청 처리와 완전히 격리 — 실패해도 무영향): // - Workers: 요청의 ~1% 에서 ctx.waitUntil 로 백그라운드 발사(응답 지연 0). diff --git a/src/lib/server/auth/guards.ts b/src/lib/server/auth/guards.ts index 7391c91..47ea6b0 100644 --- a/src/lib/server/auth/guards.ts +++ b/src/lib/server/auth/guards.ts @@ -8,7 +8,9 @@ export function requireDbContext(locals: App.Locals) { throw error(503, locals.runtimeError ?? "데이터베이스 연결을 초기화하지 못했습니다. DB 바인딩/DATABASE_URL 및 DB_DIALECT 설정을 확인해 주세요."); } - return { db: locals.db, tenant: locals.tenant }; + // rateLimitStore 는 hooks 에서 db 와 동일 블록에서 세팅되므로 db 가 있으면 반드시 존재한다. + // (레이트 리밋을 쓰지 않는 엔드포인트도 이 컨텍스트를 쓰므로 store 부재로 503 을 내지는 않는다.) + return { db: locals.db, tenant: locals.tenant, rateLimitStore: locals.rateLimitStore! }; } /** diff --git a/src/lib/server/ratelimit/index.ts b/src/lib/server/ratelimit/index.ts index 5072c9b..ef88517 100644 --- a/src/lib/server/ratelimit/index.ts +++ b/src/lib/server/ratelimit/index.ts @@ -1,5 +1,5 @@ /** - * D1 기반 슬라이딩 윈도우 레이트 리밋 (두 버킷 근사법). + * 슬라이딩 윈도우 레이트 리밋 (두 버킷 근사법). * * 윈도우를 고정 인덱스(Math.floor(now/windowMs))로 분할해 두 버킷을 유지: * - 현재 버킷 카운터를 원자적으로 증가 @@ -7,11 +7,18 @@ * count ≈ prev * (1 - elapsed/window) + current * * Fixed Window 대비 경계 burst(최대 2x) 문제를 제거. + * + * 원자증가/조회라는 상태 연산은 RateLimitStore(store.ts) 뒤로 캡슐화했고, 이 파일은 + * 윈도우 감쇠 산식만 담당한다 — 백엔드(DB/in-memory/Redis)와 무관하게 결과가 동일하다. */ -import { eq, sql } from "drizzle-orm"; -import { type DB, DB_DIALECT } from "$lib/server/db"; +import { sql } from "drizzle-orm"; +import type { DB } from "$lib/server/db"; import { rateLimits } from "$lib/server/db/schema"; +import type { RateLimitCounts, RateLimitStore } from "./store"; + +export { DbRateLimitStore, MemoryRateLimitStore, resolveRateLimitStore } from "./store"; +export type { RateLimitCounts, RateLimitStore } from "./store"; export interface RateLimitResult { allowed: boolean; @@ -26,61 +33,47 @@ export interface RateLimitOptions { limit: number; } -export async function checkRateLimit(db: DB, key: string, options: RateLimitOptions): Promise { - const now = Date.now(); +/** + * 두 버킷 카운트에 슬라이딩 윈도우 감쇠 산식을 적용해 허용 여부를 판정한다. + * store.increment / store.peek 어느 쪽에서 얻은 카운트든 동일한 산식을 쓴다. + */ +function evaluate(counts: RateLimitCounts, options: RateLimitOptions, now: number = Date.now()): RateLimitResult { const windowIndex = Math.floor(now / options.windowMs); const windowStart = windowIndex * options.windowMs; const elapsed = now - windowStart; - const currentKey = `${key}:${windowIndex}`; - const prevKey = `${key}:${windowIndex - 1}`; - // 두 윈도우가 지나면 만료 - const currentExpiresAt = new Date(windowStart + options.windowMs * 2); - - // 현재 버킷 원자적 증가. - // d1/postgres 는 upsert + RETURNING 을 지원하지만, MySQL 은 RETURNING 이 없으므로 - // ON DUPLICATE KEY UPDATE 후 재조회로 카운트를 얻는다. - // 방언별 upsert API 차이를 캐스팅으로 흡수한다 (정규 DB 타입은 활성 방언 한 종류만 - // 노출하므로, 비활성 분기의 메서드는 타입에 존재하지 않아 캐스팅이 필요하다). - let currentCount: number; - const insertValues = { key: currentKey, count: 1, expiresAt: currentExpiresAt }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const insertBuilder = db.insert(rateLimits).values(insertValues) as any; - if (DB_DIALECT === "mysql") { - // MySQL: RETURNING 미지원 → ON DUPLICATE KEY UPDATE 후 재조회. - await insertBuilder.onDuplicateKeyUpdate({ set: { count: sql`${rateLimits.count} + 1` } }); - const [row] = await db.select({ count: rateLimits.count }).from(rateLimits).where(eq(rateLimits.key, currentKey)).limit(1); - currentCount = row?.count ?? 1; - } else { - // d1 / postgres: upsert + RETURNING. - const [row] = (await insertBuilder.onConflictDoUpdate({ target: rateLimits.key, set: { count: sql`${rateLimits.count} + 1` } }).returning({ count: rateLimits.count })) as Array<{ - count: number; - }>; - currentCount = row?.count ?? 1; - } - - // 이전 버킷 조회 (best-effort) - const [prevRow] = await db.select({ count: rateLimits.count }).from(rateLimits).where(eq(rateLimits.key, prevKey)).limit(1); - - const prevCount = prevRow?.count ?? 0; - const slidingCount = Math.floor(prevCount * (1 - elapsed / options.windowMs)) + currentCount; + const slidingCount = Math.floor(counts.prev * (1 - elapsed / options.windowMs)) + counts.current; if (slidingCount > options.limit) { - return { - allowed: false, - remaining: 0, - retryAfterMs: options.windowMs - elapsed, - }; + return { allowed: false, remaining: 0, retryAfterMs: options.windowMs - elapsed }; } + return { allowed: true, remaining: Math.max(0, options.limit - slidingCount), retryAfterMs: 0 }; +} - return { - allowed: true, - remaining: Math.max(0, options.limit - slidingCount), - retryAfterMs: 0, - }; +/** + * 현재 버킷을 원자적으로 증가시키고 한도 초과 여부를 판정한다(쓰기 경로). + * 대부분의 호출부는 `if (!allowed) throw 429` 패턴으로 사용한다. + */ +export async function checkRateLimit(store: RateLimitStore, key: string, options: RateLimitOptions): Promise { + // 단일 타임스탬프를 캡처해 버킷 선택(store)과 elapsed 계산(evaluate)에 일관되게 사용한다. + const now = Date.now(); + const counts = await store.increment(key, options.windowMs, now); + return evaluate(counts, options, now); +} + +/** + * 증가 없이 현재 상태만으로 한도 초과 여부를 판정한다(read-only 경로). + * 로그인 계정 잠금처럼 "성공은 미카운트, 실패 시에만 기록" 요건에서, 인증 전에 잠금 여부를 + * 조기 판정할 때 사용한다(선증가 없이 조회). + */ +export async function peekRateLimit(store: RateLimitStore, key: string, options: RateLimitOptions): Promise { + // 단일 타임스탬프를 캡처해 버킷 선택(store)과 elapsed 계산(evaluate)에 일관되게 사용한다. + const now = Date.now(); + const counts = await store.peek(key, options.windowMs, now); + return evaluate(counts, options, now); } -/** 만료된 rate_limit 레코드를 정리 (주기적 호출 또는 훅에서 사용) */ +/** 만료된 rate_limit 레코드를 정리 (주기적 호출 또는 훅에서 사용). DbRateLimitStore(Workers) 전용. */ export async function purgeExpiredRateLimits(db: DB): Promise { await db.delete(rateLimits).where(sql`${rateLimits.expiresAt} <= ${Date.now()}`); } diff --git a/src/lib/server/ratelimit/store.ts b/src/lib/server/ratelimit/store.ts new file mode 100644 index 0000000..3d906b3 --- /dev/null +++ b/src/lib/server/ratelimit/store.ts @@ -0,0 +1,166 @@ +/** + * 레이트 리밋 저장소 추상화. + * + * `checkRateLimit`(두 버킷 슬라이딩 윈도우)의 산식은 그대로 두고, "현재 버킷 원자 증가 + + * 이전 버킷 조회"라는 상태 연산만 이 인터페이스 뒤로 캡슐화한다. 덕분에 백엔드(DB / in-memory / + * 향후 Redis·DO)를 런타임 환경에 맞춰 교체할 수 있다. + * + * 런타임 선택(resolveRateLimitStore): + * - Cloudflare Workers: isolate 는 요청 간 상태를 공유하지 못하므로 공유 저장소가 필요 → + * DB(rate_limits 테이블) 기반 DbRateLimitStore. 4방언 upsert 분기를 여기 캡슐화한다. + * - Node(adapter-node): 프로세스가 장수하므로 프로세스 내 Map 으로 충분 → + * MemoryRateLimitStore(전역 싱글턴). 핫패스에서 DB write 를 없앤다. + */ + +import { eq, sql } from "drizzle-orm"; +import { type DB, DB_DIALECT } from "$lib/server/db"; +import { rateLimits } from "$lib/server/db/schema"; + +/** 슬라이딩 윈도우 산식이 필요로 하는 두 버킷의 카운트. */ +export interface RateLimitCounts { + /** 현재 윈도우 버킷 카운트. */ + current: number; + /** 직전 윈도우 버킷 카운트(감쇠 가중치 적용 대상). */ + prev: number; +} + +/** + * 레이트 리밋 상태 저장소. + * - increment: 현재 버킷을 원자적으로 +1 하고, 증가 후 현재 카운트와 이전 버킷 카운트를 반환. + * - peek: 증가 없이 현재/이전 버킷 카운트만 조회(로그인 계정 잠금 등 read-only 판정용). + * windowMs 로부터 windowIndex(=floor(now/windowMs))를 계산하고 `${key}:${idx}` 키 규약과 + * 버킷 만료(windowStart + windowMs*2)를 각 구현이 내부적으로 관리한다. + */ +export interface RateLimitStore { + increment(key: string, windowMs: number, now?: number): Promise; + peek(key: string, windowMs: number, now?: number): Promise; +} + +/** + * `${key}:${windowIndex}` 규약 + 버킷 만료 시각을 한 번에 계산. + * `now` 를 넘기면 그 타임스탬프로 버킷 인덱스를 고정한다(미전달 시 `Date.now()`). + */ +function bucketKeys(key: string, windowMs: number, now: number = Date.now()): { currentKey: string; prevKey: string; windowStart: number } { + const windowIndex = Math.floor(now / windowMs); + return { + currentKey: `${key}:${windowIndex}`, + prevKey: `${key}:${windowIndex - 1}`, + windowStart: windowIndex * windowMs, + }; +} + +/** + * DB(rate_limits 테이블) 기반 저장소 — Cloudflare Workers 등 공유 상태가 필요한 환경용. + * 방언별 upsert 차이(d1/postgres 는 RETURNING, MySQL 은 ON DUPLICATE KEY + 재조회)를 + * 캡슐화한다. 정규 DB 타입은 활성 방언 하나만 노출하므로 비활성 분기의 메서드는 캐스팅으로 흡수. + */ +export class DbRateLimitStore implements RateLimitStore { + constructor(private readonly db: DB) {} + + async increment(key: string, windowMs: number, now?: number): Promise { + const { currentKey, prevKey, windowStart } = bucketKeys(key, windowMs, now); + // 두 윈도우가 지나면 만료. + const currentExpiresAt = new Date(windowStart + windowMs * 2); + + // 현재 버킷 원자적 증가. + // d1/postgres 는 upsert + RETURNING 을 지원하지만, MySQL 은 RETURNING 이 없으므로 + // ON DUPLICATE KEY UPDATE 후 재조회로 카운트를 얻는다. + let currentCount: number; + const insertValues = { key: currentKey, count: 1, expiresAt: currentExpiresAt }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const insertBuilder = this.db.insert(rateLimits).values(insertValues) as any; + if (DB_DIALECT === "mysql") { + await insertBuilder.onDuplicateKeyUpdate({ set: { count: sql`${rateLimits.count} + 1` } }); + const [row] = await this.db.select({ count: rateLimits.count }).from(rateLimits).where(eq(rateLimits.key, currentKey)).limit(1); + currentCount = row?.count ?? 1; + } else { + const [row] = (await insertBuilder.onConflictDoUpdate({ target: rateLimits.key, set: { count: sql`${rateLimits.count} + 1` } }).returning({ count: rateLimits.count })) as Array<{ + count: number; + }>; + currentCount = row?.count ?? 1; + } + + // 이전 버킷 조회 (best-effort). + const [prevRow] = await this.db.select({ count: rateLimits.count }).from(rateLimits).where(eq(rateLimits.key, prevKey)).limit(1); + return { current: currentCount, prev: prevRow?.count ?? 0 }; + } + + async peek(key: string, windowMs: number, now?: number): Promise { + const { currentKey, prevKey } = bucketKeys(key, windowMs, now); + const [curRow] = await this.db.select({ count: rateLimits.count }).from(rateLimits).where(eq(rateLimits.key, currentKey)).limit(1); + const [prevRow] = await this.db.select({ count: rateLimits.count }).from(rateLimits).where(eq(rateLimits.key, prevKey)).limit(1); + return { current: curRow?.count ?? 0, prev: prevRow?.count ?? 0 }; + } +} + +interface MemoryBucket { + count: number; + /** epoch ms — 이 시각 이후 버킷은 만료로 간주(조회 시 0, 스윕 시 삭제). */ + expiresAt: number; +} + +/** + * 프로세스 내 Map 기반 저장소 — Node(adapter-node) 단일 프로세스 환경용. + * DB write 없이 동일한 슬라이딩 윈도우 결과를 낸다. 만료 버킷은 조회 시 0 으로 취급하고, + * 메모리 무한 증가를 막기 위해 increment 핫패스에서 최대 1분 간격으로 전체 스윕한다. + * + * 한계: 단일 프로세스 가정 — 다중 인스턴스로 수평 확장하면 한도가 인스턴스 수만큼 완화된다 + * (공유 저장소 아님). 그 경우 RateLimitStore 를 구현하는 Redis/DO 백엔드로 교체해야 한다. + */ +export class MemoryRateLimitStore implements RateLimitStore { + private readonly buckets = new Map(); + private lastSweep = 0; + + /** 만료 키 정리 — 핫패스 O(n) 스윕을 최소 1분 간격으로만 수행. */ + private sweep(now: number): void { + if (now - this.lastSweep < 60_000) return; + this.lastSweep = now; + for (const [k, b] of this.buckets) { + if (b.expiresAt <= now) this.buckets.delete(k); + } + } + + /** 만료된 버킷은 0 으로 취급. */ + private read(fullKey: string, now: number): number { + const b = this.buckets.get(fullKey); + return b && b.expiresAt > now ? b.count : 0; + } + + async increment(key: string, windowMs: number, nowArg?: number): Promise { + const now = nowArg ?? Date.now(); + this.sweep(now); + const { currentKey, prevKey, windowStart } = bucketKeys(key, windowMs, now); + const expiresAt = windowStart + windowMs * 2; + const current = this.read(currentKey, now) + 1; + this.buckets.set(currentKey, { count: current, expiresAt }); + return { current, prev: this.read(prevKey, now) }; + } + + async peek(key: string, windowMs: number, nowArg?: number): Promise { + const now = nowArg ?? Date.now(); + const { currentKey, prevKey } = bucketKeys(key, windowMs, now); + return { current: this.read(currentKey, now), prev: this.read(prevKey, now) }; + } +} + +// Node 전역 재사용: 프로세스 내 단일 MemoryRateLimitStore 를 공유한다(HMR/다중 import 안전). +declare global { + var __keystoneRateLimitMemoryStore: MemoryRateLimitStore | undefined; +} + +function getMemoryRateLimitStore(): MemoryRateLimitStore { + if (!globalThis.__keystoneRateLimitMemoryStore) { + globalThis.__keystoneRateLimitMemoryStore = new MemoryRateLimitStore(); + } + return globalThis.__keystoneRateLimitMemoryStore; +} + +/** + * 런타임 환경에 맞는 RateLimitStore 를 반환한다. + * - Workers(platform.ctx.waitUntil 존재): 요청당 db 로 DbRateLimitStore(공유 상태). + * - Node: 프로세스 전역 MemoryRateLimitStore 싱글턴(DB write 없음). + */ +export function resolveRateLimitStore(platform: App.Platform | undefined, db: DB): RateLimitStore { + const isWorkers = typeof platform?.ctx?.waitUntil === "function"; + return isWorkers ? new DbRateLimitStore(db) : getMemoryRateLimitStore(); +} diff --git a/src/routes/(auth)/accept-invite/+page.server.ts b/src/routes/(auth)/accept-invite/+page.server.ts index a268582..39f1a8d 100644 --- a/src/routes/(auth)/accept-invite/+page.server.ts +++ b/src/routes/(auth)/accept-invite/+page.server.ts @@ -40,7 +40,7 @@ export const load: PageServerLoad = async ({ locals, url }) => { export const actions: Actions = { default: async (event) => { - const { db, tenant } = requireDbContext(event.locals); + const { db, tenant, rateLimitStore } = requireDbContext(event.locals); const locale = event.locals.locale; const formData = await event.request.formData(); @@ -50,7 +50,7 @@ export const actions: Actions = { // 토큰 제출 브루트포스 방어 — 형제 인증 라우트와 동일하게 IP 당 제한. const meta = getRequestMetadata(event); - const rl = await checkRateLimit(db, `accept-invite:${meta.ipKey}`, { windowMs: 15 * 60 * 1000, limit: 10 }); + const rl = await checkRateLimit(rateLimitStore, `accept-invite:${meta.ipKey}`, { windowMs: 15 * 60 * 1000, limit: 10 }); if (!rl.allowed) { return fail(429, { error: translate(locale, "errors.rate_limit", { minutes: Math.ceil(rl.retryAfterMs / 60000) }) }); } diff --git a/src/routes/(auth)/find-id/+page.server.ts b/src/routes/(auth)/find-id/+page.server.ts index 4ffe561..710a8b7 100644 --- a/src/routes/(auth)/find-id/+page.server.ts +++ b/src/routes/(auth)/find-id/+page.server.ts @@ -60,7 +60,7 @@ async function resolveSkinForAction(event: Parameters[0], se export const actions: Actions = { default: async (event) => { - const { db, tenant } = requireDbContext(event.locals); + const { db, tenant, rateLimitStore } = requireDbContext(event.locals); const formData = await event.request.formData(); const email = String(formData.get("email") ?? "") @@ -76,7 +76,7 @@ export const actions: Actions = { // IP 기반 레이트리밋 — 60분/5회. const meta = getRequestMetadata(event); - const rl = await checkRateLimit(db, `find-id:${meta.ipKey}`, { windowMs: 60 * 60 * 1000, limit: 5 }); + const rl = await checkRateLimit(rateLimitStore, `find-id:${meta.ipKey}`, { windowMs: 60 * 60 * 1000, limit: 5 }); if (!rl.allowed) { const msg = translate(locale, "errors.rate_limit", { minutes: Math.ceil(rl.retryAfterMs / 60000) }); return fail(429, { error: msg, skinHtml: await resolveSkinForAction(event, false, null, msg) }); diff --git a/src/routes/(auth)/find-password/+page.server.ts b/src/routes/(auth)/find-password/+page.server.ts index 760fd16..f48e424 100644 --- a/src/routes/(auth)/find-password/+page.server.ts +++ b/src/routes/(auth)/find-password/+page.server.ts @@ -63,7 +63,7 @@ async function resolveSkinForAction(event: Parameters[0], se export const actions: Actions = { default: async (event) => { - const { db, tenant } = requireDbContext(event.locals); + const { db, tenant, rateLimitStore } = requireDbContext(event.locals); const formData = await event.request.formData(); const email = String(formData.get("email") ?? "") @@ -82,7 +82,7 @@ export const actions: Actions = { // IP 기반 레이트리밋 — 60분/5회. const meta = getRequestMetadata(event); - const rl = await checkRateLimit(db, `find-password:${meta.ipKey}`, { windowMs: 60 * 60 * 1000, limit: 5 }); + const rl = await checkRateLimit(rateLimitStore, `find-password:${meta.ipKey}`, { windowMs: 60 * 60 * 1000, limit: 5 }); if (!rl.allowed) { const msg = translate(locale, "errors.rate_limit", { minutes: Math.ceil(rl.retryAfterMs / 60000) }); return fail(429, { error: msg, skinHtml: await resolveSkinForAction(event, false, undefined, msg) }); diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts index 640a9fa..471b099 100644 --- a/src/routes/(auth)/login/+page.server.ts +++ b/src/routes/(auth)/login/+page.server.ts @@ -7,10 +7,9 @@ import { authenticateLocalUser, authenticatePendingDeletionUser, hasTotpCredenti import { createMfaPendingToken, MFA_PENDING_COOKIE } from "$lib/server/auth/mfa"; import { AMR_PASSWORD, amrToAcr } from "$lib/server/auth/constants"; import { getRuntimeConfig } from "$lib/server/auth/runtime"; -import { checkRateLimit } from "$lib/server/ratelimit"; +import { checkRateLimit, peekRateLimit } from "$lib/server/ratelimit"; import { and, eq } from "drizzle-orm"; -import type { DB } from "$lib/server/db"; -import { identityProviders, rateLimits, users } from "$lib/server/db/schema"; +import { identityProviders, users } from "$lib/server/db/schema"; import { authenticateLdap } from "$lib/server/ldap/auth"; import { provisionLdapUser } from "$lib/server/ldap/provision"; import type { LdapProviderConfig } from "$lib/server/ldap/types"; @@ -26,31 +25,6 @@ import { translate } from "$lib/i18n/server"; const USER_LOCK_WINDOW_MS = 15 * 60 * 1000; const USER_LOCK_LIMIT = 10; -// checkRateLimit(두 버킷 슬라이딩 윈도우)의 키/윈도우 규약을 증가 없이 read-only 로 재현한다. -// checkRateLimit 은 호출 시 카운터를 선증가시켜 "성공 미카운트 + 실패 시에만 기록" 요건과 -// 충돌하므로, 상단 잠금 판정에는 증가 없는 조회를 쓰고 실제 기록은 실패 분기에서만 수행한다. -async function accountLockStatus(db: DB, key: string, windowMs: number, limit: number): Promise<{ locked: boolean; retryAfterMs: number }> { - const now = Date.now(); - const windowIndex = Math.floor(now / windowMs); - const windowStart = windowIndex * windowMs; - const elapsed = now - windowStart; - - const [cur] = await db - .select({ count: rateLimits.count }) - .from(rateLimits) - .where(eq(rateLimits.key, `${key}:${windowIndex}`)) - .limit(1); - const [prev] = await db - .select({ count: rateLimits.count }) - .from(rateLimits) - .where(eq(rateLimits.key, `${key}:${windowIndex - 1}`)) - .limit(1); - - const slidingCount = Math.floor((prev?.count ?? 0) * (1 - elapsed / windowMs)) + (cur?.count ?? 0); - const locked = slidingCount > limit; - return { locked, retryAfterMs: locked ? windowMs - elapsed : 0 }; -} - async function resolveSkinForAction(event: Parameters[0], flashMsg: string, redirectTo: string | null): Promise { const skinHint = event.url.searchParams.get("skinHint"); if (!skinHint || !event.locals.db || !event.locals.tenant) return null; @@ -149,12 +123,12 @@ export const actions: Actions = { }); } - const { db, tenant } = requireDbContext(event.locals); + const { db, tenant, rateLimitStore } = requireDbContext(event.locals); const requestMetadata = getRequestMetadata(event); // 레이트 리밋: IP당 10회/15분 const rlKey = `login:${requestMetadata.ipKey}`; - const rl = await checkRateLimit(db, rlKey, { windowMs: 15 * 60 * 1000, limit: 10 }); + const rl = await checkRateLimit(rateLimitStore, rlKey, { windowMs: 15 * 60 * 1000, limit: 10 }); if (!rl.allowed) { const msg = translate(locale, "login.err_rate_limit", { minutes: Math.ceil(rl.retryAfterMs / 60000) }); return fail(429, { @@ -168,9 +142,11 @@ export const actions: Actions = { // 계정 단위 잠금(S2): 인증 시도 전에 잠금 여부를 증가 없이 조회해 조기 차단한다. // (LDAP/로컬 인증 이전이라 두 경로 모두 보호되고, 잠긴 계정은 올바른 비밀번호로도 // 진입할 수 없어 scrypt 비용 낭비와 열거 오라클을 함께 차단한다.) + // peekRateLimit: 증가 없이 조회만. checkRateLimit 은 호출 시 선증가시켜 "성공 미카운트 + + // 실패 시에만 기록" 요건과 충돌하므로, 잠금 판정은 peek 로 하고 실제 기록은 실패 분기에서만 한다. const userLockKey = `login:user:${username}`; - const lock = await accountLockStatus(db, userLockKey, USER_LOCK_WINDOW_MS, USER_LOCK_LIMIT); - if (lock.locked) { + const lock = await peekRateLimit(rateLimitStore, userLockKey, { windowMs: USER_LOCK_WINDOW_MS, limit: USER_LOCK_LIMIT }); + if (!lock.allowed) { await recordAuditEvent(db, { tenantId: tenant.id, kind: "login", @@ -296,7 +272,7 @@ export const actions: Actions = { // 실패 시에만 카운트(성공은 미카운트). 미존재/존재-오답 모두 이 분기를 타므로 // 동일하게 기록되어 열거 오라클을 만들지 않는다. 임계 초과는 다음 요청의 // accountLockStatus 조기 차단에서 반영된다. - await checkRateLimit(db, userLockKey, { windowMs: USER_LOCK_WINDOW_MS, limit: USER_LOCK_LIMIT }); + await checkRateLimit(rateLimitStore, userLockKey, { windowMs: USER_LOCK_WINDOW_MS, limit: USER_LOCK_LIMIT }); await recordAuditEvent(db, { tenantId: tenant.id, diff --git a/src/routes/(auth)/mfa/+page.server.ts b/src/routes/(auth)/mfa/+page.server.ts index c6c9482..35da423 100644 --- a/src/routes/(auth)/mfa/+page.server.ts +++ b/src/routes/(auth)/mfa/+page.server.ts @@ -109,12 +109,12 @@ export const actions: Actions = { throw redirect(303, "/login"); } - if (!event.locals.db) { + if (!event.locals.db || !event.locals.rateLimitStore) { const msg = translate(locale, "errors.db_not_ready"); return fail(503, { error: msg, skinHtml: await resolveMfaSkinForAction(event, msg) }); } - const rl = await checkRateLimit(event.locals.db, `mfa:${claims.userId}`, { + const rl = await checkRateLimit(event.locals.rateLimitStore, `mfa:${claims.userId}`, { windowMs: 5 * 60 * 1000, limit: 10, }); diff --git a/src/routes/(auth)/reset-password/+page.server.ts b/src/routes/(auth)/reset-password/+page.server.ts index fecd6f3..ee7fe3b 100644 --- a/src/routes/(auth)/reset-password/+page.server.ts +++ b/src/routes/(auth)/reset-password/+page.server.ts @@ -67,7 +67,7 @@ export const load: PageServerLoad = async ({ locals, url, platform }) => { export const actions: Actions = { default: async (event) => { - const { db } = requireDbContext(event.locals); + const { db, rateLimitStore } = requireDbContext(event.locals); const formData = await event.request.formData(); const token = String(formData.get("token") ?? ""); @@ -82,7 +82,7 @@ export const actions: Actions = { // ctrls C8: 토큰 제출 브루트포스/자동화 방어. 토큰이 256bit CSPRNG 라 추측 실익은 // 낮지만, 형제 인증 라우트와 동일하게 IP 당 시도를 제한해 정합성·DB 부하를 막는다. const meta = getRequestMetadata(event); - const rl = await checkRateLimit(db, `reset-password:${meta.ipKey}`, { windowMs: 15 * 60 * 1000, limit: 10 }); + const rl = await checkRateLimit(rateLimitStore, `reset-password:${meta.ipKey}`, { windowMs: 15 * 60 * 1000, limit: 10 }); if (!rl.allowed) { return failWithSkin(translate(locale, "errors.rate_limit", { minutes: Math.ceil(rl.retryAfterMs / 60000) })); } diff --git a/src/routes/(auth)/signup/+page.server.ts b/src/routes/(auth)/signup/+page.server.ts index 2a708ae..b3519c5 100644 --- a/src/routes/(auth)/signup/+page.server.ts +++ b/src/routes/(auth)/signup/+page.server.ts @@ -60,7 +60,7 @@ async function resolveSkinForAction(event: Parameters[0], fl export const actions: Actions = { default: async (event) => { - const { db, tenant } = requireDbContext(event.locals); + const { db, tenant, rateLimitStore } = requireDbContext(event.locals); const formData = await event.request.formData(); const username = String(formData.get("username") ?? "") @@ -77,7 +77,7 @@ export const actions: Actions = { // IP 기반 레이트리밋 — 60분/5회. const meta = getRequestMetadata(event); - const rl = await checkRateLimit(db, `signup:${meta.ipKey}`, { windowMs: 60 * 60 * 1000, limit: 5 }); + const rl = await checkRateLimit(rateLimitStore, `signup:${meta.ipKey}`, { windowMs: 60 * 60 * 1000, limit: 5 }); if (!rl.allowed) { return failSkin(429, translate(locale, "signup.err_rate_limit", { minutes: Math.ceil(rl.retryAfterMs / 60000) })); } diff --git a/src/routes/(auth)/verify-email/+page.server.ts b/src/routes/(auth)/verify-email/+page.server.ts index cf4e9e8..61cf834 100644 --- a/src/routes/(auth)/verify-email/+page.server.ts +++ b/src/routes/(auth)/verify-email/+page.server.ts @@ -37,7 +37,7 @@ export const load: PageServerLoad = async ({ locals, url }) => { export const actions: Actions = { default: async (event) => { - const { db, tenant } = requireDbContext(event.locals); + const { db, tenant, rateLimitStore } = requireDbContext(event.locals); const locale = event.locals.locale; const formData = await event.request.formData(); @@ -45,7 +45,7 @@ export const actions: Actions = { // 토큰 제출 브루트포스 방어 — 형제 인증 라우트와 동일하게 IP 당 제한. const meta = getRequestMetadata(event); - const rl = await checkRateLimit(db, `verify-email:${meta.ipKey}`, { windowMs: 15 * 60 * 1000, limit: 10 }); + const rl = await checkRateLimit(rateLimitStore, `verify-email:${meta.ipKey}`, { windowMs: 15 * 60 * 1000, limit: 10 }); if (!rl.allowed) { return fail(429, { error: translate(locale, "errors.rate_limit", { minutes: Math.ceil(rl.retryAfterMs / 60000) }) }); } diff --git a/src/routes/account/profile/+page.server.ts b/src/routes/account/profile/+page.server.ts index c071784..7357036 100644 --- a/src/routes/account/profile/+page.server.ts +++ b/src/routes/account/profile/+page.server.ts @@ -74,7 +74,7 @@ export const actions: Actions = { resendVerification: async (event) => { const { locals } = event; if (!locals.user) throw redirect(303, "/login"); - const { db } = requireDbContext(locals); + const { db, rateLimitStore } = requireDbContext(locals); const locale = locals.locale; // 이미 인증된 계정은 조용히 no-op(성공 응답). @@ -82,7 +82,7 @@ export const actions: Actions = { return { resent: true }; } - const rl = await checkRateLimit(db, `resend-verification:${locals.user.id}`, { windowMs: 60 * 60 * 1000, limit: 5 }); + const rl = await checkRateLimit(rateLimitStore, `resend-verification:${locals.user.id}`, { windowMs: 60 * 60 * 1000, limit: 5 }); if (!rl.allowed) { return fail(429, { resendError: translate(locale, "errors.rate_limit", { minutes: Math.ceil(rl.retryAfterMs / 60000) }) }); } diff --git a/src/routes/admin/login/+page.server.ts b/src/routes/admin/login/+page.server.ts index 78520db..0d91e92 100644 --- a/src/routes/admin/login/+page.server.ts +++ b/src/routes/admin/login/+page.server.ts @@ -59,12 +59,12 @@ export const actions: Actions = { }); } - const { db, tenant } = requireDbContext(event.locals); + const { db, tenant, rateLimitStore } = requireDbContext(event.locals); const requestMetadata = getRequestMetadata(event); // 레이트 리밋: IP당 10회/15분 const rlKey = `admin-login:${requestMetadata.ipKey}`; - const rl = await checkRateLimit(db, rlKey, { windowMs: 15 * 60 * 1000, limit: 10 }); + const rl = await checkRateLimit(rateLimitStore, rlKey, { windowMs: 15 * 60 * 1000, limit: 10 }); if (!rl.allowed) { return fail(429, { username, diff --git a/src/routes/api/totp/enroll/confirm/+server.ts b/src/routes/api/totp/enroll/confirm/+server.ts index dfedcb1..88c3332 100644 --- a/src/routes/api/totp/enroll/confirm/+server.ts +++ b/src/routes/api/totp/enroll/confirm/+server.ts @@ -18,7 +18,7 @@ import { translate } from "$lib/i18n/server"; */ export const POST: RequestHandler = async ({ request, locals }) => { requireServiceToken(request, locals.runtimeConfig); - const { db } = requireDbContext(locals); + const { db, rateLimitStore } = requireDbContext(locals); const config = locals.runtimeConfig; if (!config.signingKeySecret) { @@ -36,7 +36,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { } // ctrls C3: enrollment 코드 브루트포스 방어 (사용자당 5분 창 10회). - const rl = await checkRateLimit(db, `totp-enroll-confirm:${userId}`, { windowMs: 5 * 60 * 1000, limit: 10 }); + const rl = await checkRateLimit(rateLimitStore, `totp-enroll-confirm:${userId}`, { windowMs: 5 * 60 * 1000, limit: 10 }); if (!rl.allowed) { throw error(429, translate(locals.locale, "totp.errors.enroll_rate_limited")); } diff --git a/src/routes/api/totp/verify/+server.ts b/src/routes/api/totp/verify/+server.ts index 068d972..91042ac 100644 --- a/src/routes/api/totp/verify/+server.ts +++ b/src/routes/api/totp/verify/+server.ts @@ -16,7 +16,7 @@ import { translate } from "$lib/i18n/server"; */ export const POST: RequestHandler = async ({ request, locals }) => { requireServiceToken(request, locals.runtimeConfig); - const { db } = requireDbContext(locals); + const { db, rateLimitStore } = requireDbContext(locals); const config = locals.runtimeConfig; if (!config.signingKeySecret) { @@ -31,7 +31,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { // ctrls C3: TOTP 브루트포스 방어. service-token 경계 안이지만 dispatcher 침해 시 // 6자리 코드를 무제한 시도해 MFA 를 우회할 수 있으므로 사용자당 시도를 제한한다. // (5분 창에 10회 — webauthn-verify 와 동일 강도.) - const rl = await checkRateLimit(db, `totp-verify:${userId}`, { windowMs: 5 * 60 * 1000, limit: 10 }); + const rl = await checkRateLimit(rateLimitStore, `totp-verify:${userId}`, { windowMs: 5 * 60 * 1000, limit: 10 }); if (!rl.allowed) { throw error(429, translate(locals.locale, "totp.errors.verify_rate_limited")); } diff --git a/src/routes/api/webauthn/authenticate/options/+server.ts b/src/routes/api/webauthn/authenticate/options/+server.ts index ff68dcc..fdade7b 100644 --- a/src/routes/api/webauthn/authenticate/options/+server.ts +++ b/src/routes/api/webauthn/authenticate/options/+server.ts @@ -8,7 +8,7 @@ import { translate } from "$lib/i18n/server"; export const POST: RequestHandler = async (event) => { const { url, locals, request } = event; - const { db, tenant } = requireDbContext(locals); + const { db, tenant, rateLimitStore } = requireDbContext(locals); const { rpID, origin } = getWebAuthnConfig(url); // Origin 검증 — 외부 사이트가 challenge 를 강탈/누적시키지 못하게. @@ -21,7 +21,7 @@ export const POST: RequestHandler = async (event) => { // webauthn_challenges 테이블을 채우는 (D1 storage exhaustion / latency 증가) // 면적을 차단. tenant + IP 키, 5분에 30회. const meta = getRequestMetadata(event); - const rl = await checkRateLimit(db, `webauthn-options:${tenant.id}:${meta.ipKey}`, { windowMs: 5 * 60 * 1000, limit: 30 }); + const rl = await checkRateLimit(rateLimitStore, `webauthn-options:${tenant.id}:${meta.ipKey}`, { windowMs: 5 * 60 * 1000, limit: 30 }); if (!rl.allowed) { throw error(429, translate(locals.locale, "webauthn.errors.rate_limited_retry", { seconds: Math.ceil(rl.retryAfterMs / 1000) })); } diff --git a/src/routes/api/webauthn/authenticate/verify/+server.ts b/src/routes/api/webauthn/authenticate/verify/+server.ts index 4f1b820..eb25202 100644 --- a/src/routes/api/webauthn/authenticate/verify/+server.ts +++ b/src/routes/api/webauthn/authenticate/verify/+server.ts @@ -51,13 +51,13 @@ export const POST: RequestHandler = async (event) => { const body = (await request.json()) as AuthenticationResponseJSON & { _redirectTo?: string }; - const { db, tenant } = requireDbContext(locals); + const { db, tenant, rateLimitStore } = requireDbContext(locals); // cf-connecting-ip 전용(H-ADMIN-3) + IPv6 /64 정규화(C6). 위조 가능한 x-forwarded-for 는 쓰지 않는다. const { ipKey } = getRequestMetadata(event); // 레이트 리밋: tenant + ip 기반 (body.id 는 클라이언트가 임의 변조 가능하므로 키로 부적합). const rlKey = `webauthn-verify:${tenant.id}:${ipKey}`; - const rl = await checkRateLimit(db, rlKey, { windowMs: 5 * 60 * 1000, limit: 10 }); + const rl = await checkRateLimit(rateLimitStore, rlKey, { windowMs: 5 * 60 * 1000, limit: 10 }); if (!rl.allowed) { throw error(429, translate(locals.locale, "webauthn.errors.auth_rate_limited")); } diff --git a/src/routes/api/webauthn/register/verify/+server.ts b/src/routes/api/webauthn/register/verify/+server.ts index ddd17b4..83fb889 100644 --- a/src/routes/api/webauthn/register/verify/+server.ts +++ b/src/routes/api/webauthn/register/verify/+server.ts @@ -62,10 +62,10 @@ export const POST: RequestHandler = async (event) => { const body = (await request.json()) as RegistrationResponseJSON & { label?: string }; const label = typeof body.label === "string" ? sanitizePasskeyLabel(body.label) : ""; - const { db: dbForRl, tenant: tenantForRl } = requireDbContext(locals); + const { tenant: tenantForRl, rateLimitStore } = requireDbContext(locals); // cf-connecting-ip 전용(H-ADMIN-3) + IPv6 /64 정규화(C6). 위조 가능한 x-forwarded-for 는 쓰지 않는다. const { ipKey } = getRequestMetadata(event); - const rl = await checkRateLimit(dbForRl, `webauthn-register-verify:${tenantForRl.id}:${ipKey}`, { windowMs: 5 * 60 * 1000, limit: 10 }); + const rl = await checkRateLimit(rateLimitStore, `webauthn-register-verify:${tenantForRl.id}:${ipKey}`, { windowMs: 5 * 60 * 1000, limit: 10 }); if (!rl.allowed) { throw error(429, translate(locals.locale, "webauthn.errors.register_rate_limited")); } diff --git a/src/routes/oidc/authorize/+server.ts b/src/routes/oidc/authorize/+server.ts index 0491f00..18a14a2 100644 --- a/src/routes/oidc/authorize/+server.ts +++ b/src/routes/oidc/authorize/+server.ts @@ -21,11 +21,11 @@ function authRedirectError(redirectUri: string, errorCode: string, description: export const GET: RequestHandler = async (event) => { const { locals, url } = event; - const { db, tenant } = requireDbContext(locals); + const { db, tenant, rateLimitStore } = requireDbContext(locals); // IP당 60회/분 제한 — grant INSERT DoS 방지 const { ip, ipKey } = getRequestMetadata(event); - const rl = await checkRateLimit(db, `oidc-authorize:${ipKey}`, { + const rl = await checkRateLimit(rateLimitStore, `oidc-authorize:${ipKey}`, { windowMs: 60 * 1000, limit: 60, }); diff --git a/src/routes/oidc/introspect/+server.ts b/src/routes/oidc/introspect/+server.ts index 12f286a..9898251 100644 --- a/src/routes/oidc/introspect/+server.ts +++ b/src/routes/oidc/introspect/+server.ts @@ -34,11 +34,11 @@ function inactive(): Response { export const POST: RequestHandler = async (event) => { const { locals, request } = event; - const { db, tenant } = requireDbContext(locals); + const { db, tenant, rateLimitStore } = requireDbContext(locals); const { signingKeySecrets } = locals.runtimeConfig; const { ipKey } = getRequestMetadata(event); - const rl = await checkRateLimit(db, `oidc-introspect:${ipKey}`, { windowMs: 60 * 1000, limit: 60 }); + const rl = await checkRateLimit(rateLimitStore, `oidc-introspect:${ipKey}`, { windowMs: 60 * 1000, limit: 60 }); if (!rl.allowed) { return new Response(JSON.stringify({ error: "rate_limit_exceeded", error_description: translate(locals.locale, "oidc.errors.rate_limited_short") }), { status: 429, diff --git a/src/routes/oidc/revoke/+server.ts b/src/routes/oidc/revoke/+server.ts index 6ac57f9..6e7b733 100644 --- a/src/routes/oidc/revoke/+server.ts +++ b/src/routes/oidc/revoke/+server.ts @@ -25,10 +25,10 @@ function errorResponse(code: string, description: string, status: number): Respo export const POST: RequestHandler = async (event) => { const { locals, request } = event; - const { db, tenant } = requireDbContext(locals); + const { db, tenant, rateLimitStore } = requireDbContext(locals); const { ipKey } = getRequestMetadata(event); - const rl = await checkRateLimit(db, `oidc-revoke:${ipKey}`, { windowMs: 60 * 1000, limit: 60 }); + const rl = await checkRateLimit(rateLimitStore, `oidc-revoke:${ipKey}`, { windowMs: 60 * 1000, limit: 60 }); if (!rl.allowed) { return new Response(JSON.stringify({ error: "rate_limit_exceeded", error_description: translate(locals.locale, "oidc.errors.rate_limited_short") }), { status: 429, diff --git a/src/routes/oidc/token/+server.ts b/src/routes/oidc/token/+server.ts index be103c8..8914a54 100644 --- a/src/routes/oidc/token/+server.ts +++ b/src/routes/oidc/token/+server.ts @@ -183,13 +183,13 @@ function tokenResponse(body: Record): Response { export const POST: RequestHandler = async (event) => { const { locals, request, url } = event; - const { db, tenant } = requireDbContext(locals); + const { db, tenant, rateLimitStore } = requireDbContext(locals); // signingKeySecret = current (발급 전용), signingKeySecrets = 복호 fallback 용. const { signingKeySecret, signingKeySecrets } = locals.runtimeConfig; // 레이트 리밋: IP당 30회/분 const { ip, ipKey, userAgent } = getRequestMetadata(event); - const rl = await checkRateLimit(db, `token:${ipKey}`, { windowMs: 60 * 1000, limit: 30 }); + const rl = await checkRateLimit(rateLimitStore, `token:${ipKey}`, { windowMs: 60 * 1000, limit: 30 }); if (!rl.allowed) { return new Response( JSON.stringify({ @@ -280,7 +280,7 @@ export const POST: RequestHandler = async (event) => { // per-client 레이트 리밋: 인증 성공한 클라이언트당 60회/분. // IP 기반 리밋과 별개로, 자격증명을 아는 남용 클라이언트를 격리한다. - const clientRl = await checkRateLimit(db, `token-client:${clientId}`, { windowMs: 60 * 1000, limit: 60 }); + const clientRl = await checkRateLimit(rateLimitStore, `token-client:${clientId}`, { windowMs: 60 * 1000, limit: 60 }); if (!clientRl.allowed) { await recordTokenFailure(clientId, "rate_limit_exceeded", "client 요청이 너무 많습니다"); return new Response( diff --git a/src/routes/saml/sso/+server.ts b/src/routes/saml/sso/+server.ts index 6cc5983..cad1c1e 100644 --- a/src/routes/saml/sso/+server.ts +++ b/src/routes/saml/sso/+server.ts @@ -432,11 +432,11 @@ async function handleIdpInitiated(event: Parameters[0], ctx: { d */ async function ssoPreflight(event: Parameters[0]) { const { locals, platform } = event; - const { db, tenant } = requireDbContext(locals); + const { db, tenant, rateLimitStore } = requireDbContext(locals); const config = getRuntimeConfig(platform); const { ipKey } = getRequestMetadata(event); - const rl = await checkRateLimit(db, `saml-sso:${ipKey}`, { windowMs: 60 * 1000, limit: 30 }); + const rl = await checkRateLimit(rateLimitStore, `saml-sso:${ipKey}`, { windowMs: 60 * 1000, limit: 30 }); if (!rl.allowed) { throw error(429, translate(locals.locale, "saml.errors.rate_limited")); } diff --git a/test/unit/ratelimit.test.ts b/test/unit/ratelimit.test.ts index 85f1f58..53186d6 100644 --- a/test/unit/ratelimit.test.ts +++ b/test/unit/ratelimit.test.ts @@ -1,52 +1,14 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import type { DB } from "$lib/server/db"; -import { checkRateLimit } from "$lib/server/ratelimit/index"; - -// ── mock 설계 요약 ──────────────────────────────────────────────────────────── -// checkRateLimit 가 활성 방언(테스트 = d1)에서 쓰는 호출 shape: -// 1) 현재 버킷 upsert: -// db.insert(rateLimits).values({key,count,expiresAt}) -// .onConflictDoUpdate({target,set}).returning({count}) → [{count}] -// 2) 이전 버킷 조회: -// db.select({count}).from(rateLimits).where(eq(key,prevKey)).limit(1) → [{count}?] -// -// 실제 계약 대응: -// - upsert 의 returning 결과로 currentCount 를 주입한다(원자적 증가 후 카운트). -// - select 결과로 prevCount 를 주입한다(이전 윈도우 버킷). -// - insertValues 를 캡처해 currentKey(`key:windowIndex`)·expiresAt 를 검증한다. -// 시간은 vi.useFakeTimers 로 고정해 windowIndex·elapsed 를 결정론적으로 만든다. -interface MakeDbOpts { - currentCount: number; - prevCount?: number; -} -function makeDb(opts: MakeDbOpts) { - let insertValues: Record | undefined; - const db = { - insert: () => ({ - values: (v: Record) => { - insertValues = v; - return { - onConflictDoUpdate: () => ({ - returning: async () => [{ count: opts.currentCount }], - }), - }; - }, - }), - select: () => ({ - from: () => ({ - where: () => ({ - limit: async () => (opts.prevCount === undefined ? [] : [{ count: opts.prevCount }]), - }), - }), - }), - }; - return { - db: db as unknown as DB, - get insertValues() { - return insertValues; - }, - }; -} +import { checkRateLimit, peekRateLimit, DbRateLimitStore, MemoryRateLimitStore, type RateLimitCounts, type RateLimitStore } from "$lib/server/ratelimit/index"; + +// ── 설계 요약 ────────────────────────────────────────────────────────────────── +// 원자증가/조회는 RateLimitStore 뒤로 캡슐화됐고, checkRateLimit/peekRateLimit 은 두 버킷 +// 카운트에 슬라이딩 윈도우 감쇠 산식을 적용하는 순수 로직이다. 따라서: +// - checkRateLimit/peekRateLimit: store 를 목으로 주입해 산식만 검증(백엔드 무관 동일 결과). +// - DbRateLimitStore: 기존 mock DB 패턴으로 upsert shape/키 규약/expiresAt 검증. +// - MemoryRateLimitStore: 실제 인스턴스로 증가·슬라이딩·evict 를 검증. +// 시간은 vi.useFakeTimers 로 고정해 windowIndex·elapsed 를 결정론적으로 만든다. // now=90000, windowMs=60000 → windowIndex=1, windowStart=60000, elapsed=30000 (윈도우 절반) const NOW = 90_000; @@ -60,11 +22,18 @@ afterEach(() => { vi.useRealTimers(); }); -describe("checkRateLimit — 한도 내/초과", () => { +// increment/peek 가 항상 고정 카운트를 반환하는 목 store — 산식만 검증하기 위한 것. +function mockStore(counts: RateLimitCounts): RateLimitStore { + return { + increment: async () => counts, + peek: async () => counts, + }; +} + +describe("checkRateLimit — 한도 내/초과 (store 주입)", () => { it("한도 내: allowed=true, remaining=limit-sliding, retryAfter=0", async () => { // prev=0, current=3 → sliding = floor(0*0.5)+3 = 3 ≤ 5 - const { db } = makeDb({ currentCount: 3, prevCount: 0 }); - const res = await checkRateLimit(db, "user:a", { windowMs: WINDOW, limit: 5 }); + const res = await checkRateLimit(mockStore({ current: 3, prev: 0 }), "user:a", { windowMs: WINDOW, limit: 5 }); expect(res.allowed).toBe(true); expect(res.remaining).toBe(2); expect(res.retryAfterMs).toBe(0); @@ -72,8 +41,7 @@ describe("checkRateLimit — 한도 내/초과", () => { it("초과: allowed=false, remaining=0, retryAfterMs=windowMs-elapsed", async () => { // prev=0, current=6 → sliding = 6 > 5 - const { db } = makeDb({ currentCount: 6, prevCount: 0 }); - const res = await checkRateLimit(db, "user:a", { windowMs: WINDOW, limit: 5 }); + const res = await checkRateLimit(mockStore({ current: 6, prev: 0 }), "user:a", { windowMs: WINDOW, limit: 5 }); expect(res.allowed).toBe(false); expect(res.remaining).toBe(0); // elapsed = 30000 → retryAfter = 60000-30000 = 30000 @@ -81,61 +49,225 @@ describe("checkRateLimit — 한도 내/초과", () => { }); }); -describe("checkRateLimit — 슬라이딩 윈도우 감쇠 산식", () => { +describe("checkRateLimit — 슬라이딩 윈도우 감쇠 산식 (기존 동일성)", () => { it("floor(prev*(1-elapsed/window)) + current 로 이전 버킷을 가중 감쇠", async () => { - // elapsed=30000, factor=1-0.5=0.5. prev=4, current=1 - // sliding = floor(4*0.5)+1 = 2+1 = 3 ≤ 5 → 허용, remaining=2 - const { db } = makeDb({ currentCount: 1, prevCount: 4 }); - const res = await checkRateLimit(db, "user:a", { windowMs: WINDOW, limit: 5 }); + // elapsed=30000, factor=1-0.5=0.5. prev=4, current=1 → floor(4*0.5)+1 = 3 ≤ 5 → 허용, remaining=2 + const res = await checkRateLimit(mockStore({ current: 1, prev: 4 }), "user:a", { windowMs: WINDOW, limit: 5 }); expect(res.allowed).toBe(true); - // remaining 이 current(1)만이 아니라 감쇠된 prev(2)까지 반영해야 진짜 슬라이딩. expect(res.remaining).toBe(2); }); it("감쇠된 prev 가중치로 한도를 넘으면 차단", async () => { // prev=10, current=1, factor=0.5 → floor(5)+1 = 6 > 5 → 차단 - const { db } = makeDb({ currentCount: 1, prevCount: 10 }); - const res = await checkRateLimit(db, "user:a", { windowMs: WINDOW, limit: 5 }); + const res = await checkRateLimit(mockStore({ current: 1, prev: 10 }), "user:a", { windowMs: WINDOW, limit: 5 }); expect(res.allowed).toBe(false); expect(res.remaining).toBe(0); }); it("elapsed 비율에 따라 감쇠 강도가 달라진다(윈도우 초반엔 prev 가중치 큼)", async () => { - // now=61000 → elapsed=1000, factor≈1-1000/60000. prev=5, current=1 - // floor(5*(1-1000/60000))+1 = floor(4.916..)+1 = 4+1 = 5 ≤ 5 → 허용(경계) + // now=61000 → elapsed=1000. prev=5, current=1 → floor(5*(1-1000/60000))+1 = floor(4.916..)+1 = 5 ≤ 5 → 허용(경계) vi.setSystemTime(new Date(61_000)); - const { db } = makeDb({ currentCount: 1, prevCount: 5 }); - const res = await checkRateLimit(db, "user:a", { windowMs: WINDOW, limit: 5 }); + const res = await checkRateLimit(mockStore({ current: 1, prev: 5 }), "user:a", { windowMs: WINDOW, limit: 5 }); expect(res.allowed).toBe(true); expect(res.remaining).toBe(0); }); + + it("이전 버킷이 없으면(prev=0) sliding=current", async () => { + const res = await checkRateLimit(mockStore({ current: 5, prev: 0 }), "user:a", { windowMs: WINDOW, limit: 5 }); + expect(res.allowed).toBe(true); + expect(res.remaining).toBe(0); // 5-5 + }); }); -describe("checkRateLimit — 키 네임스페이싱/버킷 만료", () => { - it("current 버킷 key = `${key}:${windowIndex}`, expiresAt = windowStart + windowMs*2", async () => { - const store = makeDb({ currentCount: 1, prevCount: 0 }); - await checkRateLimit(store.db, "user:a", { windowMs: WINDOW, limit: 5 }); - expect(store.insertValues?.key).toBe("user:a:1"); // windowIndex = floor(90000/60000) = 1 +describe("peekRateLimit — 동일 산식, 증가 없음", () => { + it("peek 결과에 checkRateLimit 과 동일한 산식을 적용한다", async () => { + const res = await peekRateLimit(mockStore({ current: 6, prev: 0 }), "user:a", { windowMs: WINDOW, limit: 5 }); + expect(res.allowed).toBe(false); // 6 > 5 + expect(res.retryAfterMs).toBe(WINDOW - 30_000); + }); + + it("store.increment 가 아니라 store.peek 를 호출한다(증가 부작용 없음)", async () => { + const increment = vi.fn(); + const peek = vi.fn(async () => ({ current: 1, prev: 0 }) as RateLimitCounts); + const store: RateLimitStore = { increment, peek }; + await peekRateLimit(store, "user:a", { windowMs: WINDOW, limit: 5 }); + expect(peek).toHaveBeenCalledOnce(); + expect(increment).not.toHaveBeenCalled(); + }); +}); + +describe("단일 타임스탬프 시맨틱 — increment/peek/evaluate 가 동일 now 사용", () => { + it("checkRateLimit 은 store.increment 와 evaluate 에 동일한 now(=Date.now())를 전달한다", async () => { + let seenNow: number | undefined; + const store: RateLimitStore = { + increment: async (_k, _w, now) => { + seenNow = now; + return { current: 6, prev: 0 }; + }, + peek: async () => ({ current: 0, prev: 0 }), + }; + const res = await checkRateLimit(store, "user:a", { windowMs: WINDOW, limit: 5 }); + // store 가 받은 now 로 windowIndex=1, windowStart=60000, elapsed=30000 이 결정되고 + // evaluate 도 같은 now 를 써야 retryAfterMs = WINDOW-30000 이 나온다. + expect(seenNow).toBe(NOW); + expect(res.allowed).toBe(false); + expect(res.retryAfterMs).toBe(WINDOW - 30_000); + }); + + it("peekRateLimit 도 store.peek 와 evaluate 에 동일한 now 를 전달한다", async () => { + let seenNow: number | undefined; + const store: RateLimitStore = { + increment: async () => ({ current: 0, prev: 0 }), + peek: async (_k, _w, now) => { + seenNow = now; + return { current: 6, prev: 0 }; + }, + }; + const res = await peekRateLimit(store, "user:a", { windowMs: WINDOW, limit: 5 }); + expect(seenNow).toBe(NOW); + expect(res.retryAfterMs).toBe(WINDOW - 30_000); + }); + + it("윈도우 경계 직전/직후: 명시적 now 로 버킷 인덱스가 결정론적으로 갈린다", async () => { + const store = new MemoryRateLimitStore(); + // 경계 직전: now=119999 → windowIndex=1 → 키 k:1 + const before = await store.increment("k", WINDOW, 2 * WINDOW - 1); + expect(before).toEqual({ current: 1, prev: 0 }); + // 경계 직후: now=120000 → windowIndex=2 → 새 버킷 k:2 (직전 k:1 은 prev 로 노출) + const after = await store.increment("k", WINDOW, 2 * WINDOW); + expect(after).toEqual({ current: 1, prev: 1 }); + }); +}); + +// ── DbRateLimitStore (mock DB) ──────────────────────────────────────────────── +// 테스트 방언 = d1 → onConflictDoUpdate + RETURNING 경로. +// increment: insert(...).values(v).onConflictDoUpdate(...).returning() → [{count}] +// 그 뒤 이전 버킷 select 1회. +// peek: current select → prev select (증가 없음). +// select 결과는 호출 순서대로 selectRows 큐에서 소비한다. +interface MakeDbOpts { + currentCount: number; + selectRows?: Array>; +} +function makeDb(opts: MakeDbOpts) { + let insertValues: Record | undefined; + let selectCall = 0; + const selectRows = opts.selectRows ?? []; + const db = { + insert: () => ({ + values: (v: Record) => { + insertValues = v; + return { + onConflictDoUpdate: () => ({ + returning: async () => [{ count: opts.currentCount }], + }), + onDuplicateKeyUpdate: async () => undefined, + }; + }, + }), + select: () => ({ + from: () => ({ + where: () => ({ + limit: async () => selectRows[selectCall++] ?? [], + }), + }), + }), + }; + return { + db: db as unknown as DB, + get insertValues() { + return insertValues; + }, + }; +} + +describe("DbRateLimitStore — upsert shape / 키 규약 / 만료", () => { + it("increment: current 는 upsert RETURNING, prev 는 이전 버킷 select", async () => { + // prev 버킷 select → [{count:4}] + const store = makeDb({ currentCount: 3, selectRows: [[{ count: 4 }]] }); + const counts = await new DbRateLimitStore(store.db).increment("user:a", WINDOW); + expect(counts).toEqual({ current: 3, prev: 4 }); + }); + + it("increment: current 버킷 key=`${key}:${windowIndex}`, count=1, expiresAt=windowStart+windowMs*2", async () => { + const store = makeDb({ currentCount: 1, selectRows: [[]] }); + await new DbRateLimitStore(store.db).increment("user:a", WINDOW); + expect(store.insertValues?.key).toBe("user:a:1"); // floor(90000/60000)=1 expect(store.insertValues?.count).toBe(1); - // windowStart(60000) + windowMs*2(120000) = 180000 - expect((store.insertValues?.expiresAt as Date).getTime()).toBe(60_000 + WINDOW * 2); + expect((store.insertValues?.expiresAt as Date).getTime()).toBe(60_000 + WINDOW * 2); // 180000 }); - it("서로 다른 키는 서로 다른 버킷 key 로 격리된다", async () => { - const a = makeDb({ currentCount: 1, prevCount: 0 }); - await checkRateLimit(a.db, "user:a", { windowMs: WINDOW, limit: 5 }); - const b = makeDb({ currentCount: 1, prevCount: 0 }); - await checkRateLimit(b.db, "user:b", { windowMs: WINDOW, limit: 5 }); - expect(a.insertValues?.key).toBe("user:a:1"); - expect(b.insertValues?.key).toBe("user:b:1"); - expect(a.insertValues?.key).not.toBe(b.insertValues?.key); + it("increment: 이전 버킷 row 부재 → prev=0", async () => { + const store = makeDb({ currentCount: 5, selectRows: [[]] }); + const counts = await new DbRateLimitStore(store.db).increment("user:a", WINDOW); + expect(counts).toEqual({ current: 5, prev: 0 }); }); - it("이전 버킷이 없으면(prev row 부재) prevCount=0 으로 취급", async () => { - // prevCount undefined → select 빈 배열 → prevCount=0, sliding=current - const { db } = makeDb({ currentCount: 5 }); - const res = await checkRateLimit(db, "user:a", { windowMs: WINDOW, limit: 5 }); - expect(res.allowed).toBe(true); - expect(res.remaining).toBe(0); // 5-5 + it("peek: 증가 없이 current/prev 두 select 만 수행", async () => { + // 첫 select = current 버킷, 둘째 = prev 버킷 + const store = makeDb({ currentCount: 999, selectRows: [[{ count: 2 }], [{ count: 7 }]] }); + const counts = await new DbRateLimitStore(store.db).peek("user:a", WINDOW); + expect(counts).toEqual({ current: 2, prev: 7 }); + // insert(upsert)는 호출되지 않았어야 한다 → insertValues 미설정 + expect(store.insertValues).toBeUndefined(); + }); +}); + +// ── MemoryRateLimitStore ────────────────────────────────────────────────────── +describe("MemoryRateLimitStore — 증가/슬라이딩/evict", () => { + it("increment 는 현재 버킷을 원자 증가하고 prev 를 함께 반환", async () => { + const store = new MemoryRateLimitStore(); + expect(await store.increment("k", WINDOW)).toEqual({ current: 1, prev: 0 }); + expect(await store.increment("k", WINDOW)).toEqual({ current: 2, prev: 0 }); + expect(await store.increment("k", WINDOW)).toEqual({ current: 3, prev: 0 }); + }); + + it("이전 윈도우 버킷 카운트를 prev 로 노출한다", async () => { + const store = new MemoryRateLimitStore(); + // windowIndex 0 (t=30000) 에서 2회 증가 → k:0 count=2, expiresAt=120000 + vi.setSystemTime(new Date(30_000)); + await store.increment("k", WINDOW); + await store.increment("k", WINDOW); + // windowIndex 1 (t=90000) 에서 조회 → prev=k:0=2, current=k:1=0 + vi.setSystemTime(new Date(NOW)); + expect(await store.peek("k", WINDOW)).toEqual({ current: 0, prev: 2 }); + }); + + it("checkRateLimit 과 결합 시 DB 백엔드와 동일한 한도 판정을 낸다", async () => { + const store = new MemoryRateLimitStore(); + const opts = { windowMs: WINDOW, limit: 5 }; + // prev=0 이므로 sliding=current. 5회까지 허용, 6회째 초과. + for (let i = 1; i <= 5; i++) { + const r = await checkRateLimit(store, "user:a", opts); + expect(r.allowed).toBe(true); + expect(r.remaining).toBe(5 - i); + } + const sixth = await checkRateLimit(store, "user:a", opts); + expect(sixth.allowed).toBe(false); + expect(sixth.retryAfterMs).toBe(WINDOW - 30_000); + }); + + it("만료 버킷은 조회 시 0 으로 취급된다", async () => { + const store = new MemoryRateLimitStore(); + // t=90000: k:1 expiresAt = 60000 + 120000 = 180000 + await store.increment("k", WINDOW); + // t=200000 > 180000 → 만료 + vi.setSystemTime(new Date(200_000)); + expect(await store.peek("k", WINDOW)).toEqual({ current: 0, prev: 0 }); + }); + + it("evict: 만료 버킷은 스윕에서 실제로 제거되어 메모리가 누적되지 않는다", async () => { + const store = new MemoryRateLimitStore(); + // t=90000: 첫 sweep(lastSweep 0→90000, 빈 맵), k:1 저장(expiresAt 180000) + await store.increment("k", WINDOW); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((store as any).buckets.size).toBe(1); + // t=250000: sweep 간격(≥60s) 경과 + k:1 만료(180000<250000) → k 제거, other 만 남음 + vi.setSystemTime(new Date(250_000)); + await store.increment("other", WINDOW); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const buckets = (store as any).buckets as Map; + expect(buckets.size).toBe(1); + expect([...buckets.keys()][0]).toBe(`other:${Math.floor(250_000 / WINDOW)}`); }); }); From e427fdddeec4ad0380dfe42513b8889f898535b0 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 17:43:34 +0900 Subject: [PATCH 20/26] =?UTF-8?q?feat(a11y):=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=ED=8F=BC=20=EC=97=90=EB=9F=AC=20=EB=B0=B0=EB=84=88=C2=B7?= =?UTF-8?q?=EB=A1=9C=EB=94=A9=20=EC=83=81=ED=83=9C=C2=B7=EC=8A=A4=ED=82=A8?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B3=B5=EA=B5=AC=20(P14)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FormError.svelte 신설: role="alert"+aria-live="assertive"+실패 시 포커스 이동 공용 에러 배너 - 8개 인증 폼(login/signup/mfa/reset-password/find-id/find-password/accept-invite/verify-email)에 FormError 적용 + use:enhance 로딩 상태(제출 중 버튼 disabled·스피너, SSR 폴백 유지) - login: 스킨 렌더 조건에 복구(soft-delete) 케이스 예외 — 스킨 로그인에서도 계정 복구 패널 도달(U3) - 순수 UI 변경(progressive enhancement, 시각 동작 무회귀). profile.svelte 가 FormError 를 참조하므로 P13 앞에 배치. Co-Authored-By: Claude Fable 5 --- src/lib/components/FormError.svelte | 30 ++++++++++ src/routes/(auth)/accept-invite/+page.svelte | 33 ++++++++-- src/routes/(auth)/find-id/+page.svelte | 33 ++++++++-- src/routes/(auth)/find-password/+page.svelte | 33 ++++++++-- src/routes/(auth)/login/+page.svelte | 60 +++++++++++++------ src/routes/(auth)/mfa/+page.svelte | 33 +++++++--- src/routes/(auth)/reset-password/+page.svelte | 33 ++++++++-- src/routes/(auth)/signup/+page.svelte | 33 ++++++++-- src/routes/(auth)/verify-email/+page.svelte | 33 ++++++++-- 9 files changed, 259 insertions(+), 62 deletions(-) create mode 100644 src/lib/components/FormError.svelte diff --git a/src/lib/components/FormError.svelte b/src/lib/components/FormError.svelte new file mode 100644 index 0000000..5c492e9 --- /dev/null +++ b/src/lib/components/FormError.svelte @@ -0,0 +1,30 @@ + + +{#if message} + +{/if} diff --git a/src/routes/(auth)/accept-invite/+page.svelte b/src/routes/(auth)/accept-invite/+page.svelte index f906d28..44370bc 100644 --- a/src/routes/(auth)/accept-invite/+page.svelte +++ b/src/routes/(auth)/accept-invite/+page.svelte @@ -1,12 +1,24 @@
@@ -26,10 +38,8 @@ const accepted = $derived((form as { accepted?: boolean } | null)?.accepted ?? f {t("accept_invite.go_login")} → {:else if data.valid} - {#if err} -
{err}
- {/if} - + +
@@ -54,8 +64,19 @@ const accepted = $derived((form as { accepted?: boolean } | null)?.accepted ?? f autocomplete="new-password" class="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none" />
- {:else} diff --git a/src/routes/(auth)/find-id/+page.svelte b/src/routes/(auth)/find-id/+page.svelte index d344631..a8fbe59 100644 --- a/src/routes/(auth)/find-id/+page.svelte +++ b/src/routes/(auth)/find-id/+page.svelte @@ -1,12 +1,24 @@ -{#if skinHtmlEffective} - +{#if skinHtmlEffective && !form?.recovery} + {#if !data.dbReady && data.runtimeError}
{data.runtimeError} @@ -126,17 +140,9 @@ async function loginWithPasskey() {
{/if} - {#if form?.error} -
- {form.error} -
- {/if} + - {#if passkeyError} -
- {passkeyError} -
- {/if} + {#if form?.recovery} @@ -144,7 +150,7 @@ async function loginWithPasskey() { {t("login.recovery_desc")}
-
+ @@ -164,8 +170,17 @@ async function loginWithPasskey() {
@@ -174,7 +189,7 @@ async function loginWithPasskey() { {t("login.recovery_cancel")} {:else} -
+
@@ -206,8 +221,17 @@ async function loginWithPasskey() { diff --git a/src/routes/(auth)/mfa/+page.svelte b/src/routes/(auth)/mfa/+page.svelte index 746b02f..ce7557c 100644 --- a/src/routes/(auth)/mfa/+page.svelte +++ b/src/routes/(auth)/mfa/+page.svelte @@ -1,12 +1,24 @@
@@ -27,14 +39,23 @@ const isSuccess = $derived(verified || (data.valid && data.alreadyVerified)); {t("verify_email.go_login")} → {:else if data.valid} - {#if err} -
{err}
- {/if} +

{t("verify_email.confirm_prompt")}

-
+ -
{:else} From 428980ffc694e2b855bb122015d07cd437620002 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 17:44:11 +0900 Subject: [PATCH 21/26] =?UTF-8?q?feat(account):=20=EB=A9=94=EC=9D=BC=20loc?= =?UTF-8?q?ale=20=EC=9D=B8=EC=A7=80=C2=B7=EC=84=B8=EC=85=98=20=EC=B2=A0?= =?UTF-8?q?=ED=9A=8C=20=EC=95=8C=EB=A6=BC=C2=B7=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EB=B3=80=EA=B2=BD=20(P13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - email.ts: 트랜잭션 메일 전량 수신자 locale 인지 — 제목·본문·CTA·푸터·lang 속성 i18n(baseHtml/ctaBody 공통화). 발송 헬퍼(issueEmailVerification·issueInvite)·호출부(find-id·find-password·signup·admin/users 초대)에서 대상 locale 전달, i18n core.normalizeLocale 공용화 - 세션 철회 보안 알림: security-notify 에 session_revoked·sessions_revoked_all·email_change_requested kind 추가, account/sessions 개별/일괄 철회에 dispatchSecurityAlert(본인 직접 철회도 발송) + 하드코딩 한국어 2건 i18n - 프로필 이메일 변경(F3): email-change.ts(신규, email_change_tokens 1회용)·confirm-email-change 라우트(신규), profile changeEmail 액션 — 현 비밀번호 재인증→중복 검증→pendingEmail 저장→새 주소 확인 메일→기존 주소 변경시도 알림, profile.svelte 이메일 변경 UI(대기 배너·FormError) - email-verification 테스트를 새 locale 시그니처로 갱신. gc.ts 의 email_change purge 는 P12 커밋에 통합됨. sessions i18n·security-notify backup kind 는 이 커밋과 P17 이 공유해 여기 통합. - 생년월일 형식 에러 i18n(P12-6)은 profile 파일 분할을 줄이려 이 커밋에 편승. Co-Authored-By: Claude Fable 5 --- src/lib/i18n/core.ts | 7 ++ src/lib/server/auth/email-change.ts | 48 ++++++++++ src/lib/server/auth/email-verification.ts | 5 +- src/lib/server/auth/invite.ts | 5 +- src/lib/server/email.ts | 95 ++++++++++-------- src/lib/server/security-notify.ts | 19 ++-- src/routes/(auth)/find-id/+page.server.ts | 5 +- .../(auth)/find-password/+page.server.ts | 5 +- src/routes/(auth)/signup/+page.server.ts | 2 +- .../confirm-email-change/+page.server.ts | 96 +++++++++++++++++++ .../account/confirm-email-change/+page.svelte | 49 ++++++++++ src/routes/account/profile/+page.server.ts | 78 ++++++++++++++- src/routes/account/profile/+page.svelte | 64 +++++++++++++ src/routes/account/sessions/+page.server.ts | 15 ++- src/routes/admin/users/+page.server.ts | 3 +- test/unit/email-verification.test.ts | 8 +- 16 files changed, 439 insertions(+), 65 deletions(-) create mode 100644 src/lib/server/auth/email-change.ts create mode 100644 src/routes/account/confirm-email-change/+page.server.ts create mode 100644 src/routes/account/confirm-email-change/+page.svelte diff --git a/src/lib/i18n/core.ts b/src/lib/i18n/core.ts index 41f1219..09ef687 100644 --- a/src/lib/i18n/core.ts +++ b/src/lib/i18n/core.ts @@ -44,6 +44,13 @@ function interpolate(message: string, params?: Record): }); } +// 임의의 locale 문자열("en-US"/"ko-KR"/null 등)을 지원 Locale(ko|en)로 정규화한다. +// en* 만 en, 그 외(미상/null 포함)는 ko 기본. 서버 알림/메일에서 수신자 locale 해석에 공유한다. +// 근거: 지원 Locale 은 ko|en 뿐이며, 미상 locale 은 FALLBACK_LOCALE(ko)로 안전하게 수렴시킨다. +export function normalizeLocale(locale: string | null | undefined): Locale { + return (locale ?? "").toLowerCase().startsWith("en") ? "en" : FALLBACK_LOCALE; +} + // 로케일 + 키 경로를 실제 메시지로 해석한다. 클라이언트 t() 와 서버 translate() 가 공유하는 단일 lookup 진입점. // 현재 로케일 → ko 폴백 → 원본 key 순으로 해석한다. export function resolveMessage(locale: Locale, key: string, params?: Record): string { diff --git a/src/lib/server/auth/email-change.ts b/src/lib/server/auth/email-change.ts new file mode 100644 index 0000000..b4ec290 --- /dev/null +++ b/src/lib/server/auth/email-change.ts @@ -0,0 +1,48 @@ +import { env } from "$env/dynamic/private"; +import { and, eq, isNull } from "drizzle-orm"; +import type { Locale } from "$lib/i18n/core"; +import type { DB } from "$lib/server/db"; +import { emailChangeTokens } from "$lib/server/db/schema"; +import { generateToken, sendEmailChangeVerificationEmail } from "$lib/server/email"; + +export const EMAIL_CHANGE_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24시간 + +/** + * F3: 이메일 변경 확인 토큰 발급 + 새 주소로 확인 메일 발송(best-effort, 완전 격리). + * issueEmailVerification 과 동일한 격리 규약을 따른다: + * - issuer(IDP_ISSUER_URL) 미설정 시 발송 스킵(host header injection 방지 목적). + * - Workers 는 waitUntil 로 응답 경로에서 분리, Node 는 await. + * - 토큰 발급/발송 예외는 삼켜서 호출부(변경 요청 응답)에 전파하지 않는다. + * + * 토큰에는 변경 대상 주소(targetEmail)를 바인딩한다 — 확인 라우트가 이 값으로만 email 을 + * 교체하므로, 링크가 다른 주소로 재사용되거나 대기값이 바뀌어도 확인 대상이 어긋나지 않는다. + * 같은 user 의 기존 미사용 변경 토큰은 새 토큰 발급 전에 모두 소진해 최신 요청만 유효하게 한다. + */ +export async function issueEmailChange(db: DB, userId: string, targetEmail: string, locale: Locale, platform: App.Platform | undefined): Promise { + const issuer = env.IDP_ISSUER_URL?.replace(/\.+$/, "").replace(/\/+$/, ""); + if (!issuer) { + console.error("[email-change] IDP_ISSUER_URL 미설정 — 이메일 변경 확인 메일 발송 불가"); + return; + } + try { + // 같은 user 의 미사용 변경 토큰을 모두 소진 처리 → 새 토큰만 유효. + await db + .update(emailChangeTokens) + .set({ usedAt: new Date() }) + .where(and(eq(emailChangeTokens.userId, userId), isNull(emailChangeTokens.usedAt))); + + const { token, tokenHash } = await generateToken(); + const expiresAt = new Date(Date.now() + EMAIL_CHANGE_EXPIRY_MS); + await db.insert(emailChangeTokens).values({ userId, tokenHash, targetEmail, expiresAt }); + + const confirmUrl = `${issuer}/account/confirm-email-change?token=${encodeURIComponent(token)}`; + const sendPromise = sendEmailChangeVerificationEmail(targetEmail, confirmUrl, locale, platform).catch(() => { + // 발송 실패는 조용히 무시 + }); + const wait = platform?.ctx?.waitUntil?.bind(platform.ctx); + if (wait) wait(sendPromise); + else await sendPromise; + } catch { + // 토큰 발급/발송 실패가 상위 흐름을 실패시키지 않도록 격리 + } +} diff --git a/src/lib/server/auth/email-verification.ts b/src/lib/server/auth/email-verification.ts index e3d2c98..b96089c 100644 --- a/src/lib/server/auth/email-verification.ts +++ b/src/lib/server/auth/email-verification.ts @@ -1,4 +1,5 @@ import { env } from "$env/dynamic/private"; +import type { Locale } from "$lib/i18n/core"; import type { DB } from "$lib/server/db"; import { emailVerificationTokens } from "$lib/server/db/schema"; import { generateToken, sendEmailVerificationEmail } from "$lib/server/email"; @@ -12,7 +13,7 @@ export const EMAIL_VERIFY_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24시간 * - Workers 는 waitUntil 로 응답 경로에서 분리, Node 는 await. * - 토큰 발급/발송 예외는 삼켜서 호출부에 전파하지 않는다. */ -export async function issueEmailVerification(db: DB, userId: string, email: string, platform: App.Platform | undefined): Promise { +export async function issueEmailVerification(db: DB, userId: string, email: string, locale: Locale, platform: App.Platform | undefined): Promise { const issuer = env.IDP_ISSUER_URL?.replace(/\.+$/, "").replace(/\/+$/, ""); if (!issuer) { console.error("[email-verification] IDP_ISSUER_URL 미설정 — 이메일 인증 메일 발송 불가"); @@ -23,7 +24,7 @@ export async function issueEmailVerification(db: DB, userId: string, email: stri const expiresAt = new Date(Date.now() + EMAIL_VERIFY_EXPIRY_MS); await db.insert(emailVerificationTokens).values({ userId, tokenHash, expiresAt }); const verifyUrl = `${issuer}/verify-email?token=${encodeURIComponent(token)}`; - const sendPromise = sendEmailVerificationEmail(email, verifyUrl, platform).catch(() => { + const sendPromise = sendEmailVerificationEmail(email, verifyUrl, locale, platform).catch(() => { // 발송 실패는 조용히 무시 }); const wait = platform?.ctx?.waitUntil?.bind(platform.ctx); diff --git a/src/lib/server/auth/invite.ts b/src/lib/server/auth/invite.ts index 4539cd4..02c7da2 100644 --- a/src/lib/server/auth/invite.ts +++ b/src/lib/server/auth/invite.ts @@ -1,4 +1,5 @@ import { env } from "$env/dynamic/private"; +import type { Locale } from "$lib/i18n/core"; import type { DB } from "$lib/server/db"; import { inviteTokens } from "$lib/server/db/schema"; import { generateToken, sendInviteEmail } from "$lib/server/email"; @@ -12,7 +13,7 @@ export const INVITE_EXPIRY_MS = 72 * 60 * 60 * 1000; // 72시간 * - Workers 는 waitUntil 로 응답 경로에서 분리, Node 는 await. * - 토큰 발급/발송 예외는 삼켜서 호출부(계정 생성 응답)에 전파하지 않는다(best-effort). */ -export async function issueInvite(db: DB, userId: string, email: string, platform: App.Platform | undefined): Promise { +export async function issueInvite(db: DB, userId: string, email: string, locale: Locale, platform: App.Platform | undefined): Promise { const issuer = env.IDP_ISSUER_URL?.replace(/\.+$/, "").replace(/\/+$/, ""); if (!issuer) { console.error("[invite] IDP_ISSUER_URL 미설정 — 초대 메일 발송 불가"); @@ -23,7 +24,7 @@ export async function issueInvite(db: DB, userId: string, email: string, platfor const expiresAt = new Date(Date.now() + INVITE_EXPIRY_MS); await db.insert(inviteTokens).values({ userId, tokenHash, expiresAt }); const inviteUrl = `${issuer}/accept-invite?token=${encodeURIComponent(token)}`; - const sendPromise = sendInviteEmail(email, inviteUrl, platform).catch(() => { + const sendPromise = sendInviteEmail(email, inviteUrl, locale, platform).catch(() => { // 발송 실패는 조용히 무시 }); const wait = platform?.ctx?.waitUntil?.bind(platform.ctx); diff --git a/src/lib/server/email.ts b/src/lib/server/email.ts index 6b7b245..ce614de 100644 --- a/src/lib/server/email.ts +++ b/src/lib/server/email.ts @@ -1,4 +1,6 @@ import { env } from "$env/dynamic/private"; +import type { Locale } from "$lib/i18n/core"; +import { translate } from "$lib/i18n/server"; // ── 발송 코어: 런타임 분기 ──────────────────────────────────────────────────── // B6: nodemailer(raw TCP SMTP)는 Cloudflare Workers 런타임에서 동작하지 않는다 @@ -121,79 +123,96 @@ function safeAbsoluteUrl(url: string): string | null { } } -function baseHtml(title: string, body: string): string { +// F1: 트랜잭션 메일 전량 수신자 locale 인지. 제목·본문·버튼·푸터·lang 속성을 모두 +// i18n(translate) 로 렌더한다(보안 알림 메일과 동일한 locale-aware 원칙). i18n 키는 +// `email.*` 네임스페이스(ko/en 대칭). lang 속성은 지원 Locale(ko|en)을 그대로 쓴다. +function baseHtml(locale: Locale, title: string, body: string, footer: string): string { return ` - +

${escapeHtml(title)}

${body} -

본인이 요청하지 않았다면 이 이메일을 무시해 주세요.

+

${escapeHtml(footer)}

`; } -export async function sendFindIdEmail(to: string, username: string, platform?: App.Platform): Promise { +// 버튼(CTA) 링크가 들어가는 본문 조각을 공통 구성한다. safeUrl 은 호출부에서 scheme 검증을 마친 값. +function ctaBody(bodyText: string, safeUrl: string, buttonLabel: string): string { + return `

${escapeHtml(bodyText)}

+

+ ${escapeHtml(buttonLabel)} +

`; +} + +export async function sendFindIdEmail(to: string, username: string, locale: Locale, platform?: App.Platform): Promise { + const footer = translate(locale, "email.footer"); + const intro = translate(locale, "email.find_id.intro"); const html = baseHtml( - "아이디 확인", - `

요청하신 아이디 정보입니다.

+ locale, + translate(locale, "email.find_id.title"), + `

${escapeHtml(intro)}

${escapeHtml(username)}

`, + footer, ); - const text = `요청하신 아이디 정보입니다.\n\n${username}\n\n본인이 요청하지 않았다면 이 이메일을 무시해 주세요.`; - await send(to, "아이디 안내", html, text, platform); + const text = `${intro}\n\n${username}\n\n${footer}`; + await send(to, translate(locale, "email.find_id.subject"), html, text, platform); } -export async function sendPasswordResetEmail(to: string, resetUrl: string, platform?: App.Platform): Promise { +export async function sendPasswordResetEmail(to: string, resetUrl: string, locale: Locale, platform?: App.Platform): Promise { const safeUrl = safeAbsoluteUrl(resetUrl); if (!safeUrl) { // 잘못된 URL 형식이면 메일 발송 자체 거부 — silent skip 으로 user enumeration 차단. console.error("[email] sendPasswordResetEmail: 잘못된 resetUrl scheme — 발송 취소"); return; } - const html = baseHtml( - "비밀번호 재설정", - `

아래 버튼을 클릭하여 비밀번호를 재설정하세요. 링크는 1시간 동안 유효합니다.

-

- 비밀번호 재설정 -

`, - ); - const text = `아래 링크에서 비밀번호를 재설정하세요. 링크는 1시간 동안 유효합니다.\n\n${safeUrl}\n\n본인이 요청하지 않았다면 이 이메일을 무시해 주세요.`; - await send(to, "비밀번호 재설정 안내", html, text, platform); + const footer = translate(locale, "email.footer"); + const body = translate(locale, "email.password_reset.body"); + const html = baseHtml(locale, translate(locale, "email.password_reset.title"), ctaBody(body, safeUrl, translate(locale, "email.password_reset.button")), footer); + const text = `${translate(locale, "email.password_reset.text")}\n\n${safeUrl}\n\n${footer}`; + await send(to, translate(locale, "email.password_reset.subject"), html, text, platform); } -export async function sendEmailVerificationEmail(to: string, verifyUrl: string, platform?: App.Platform): Promise { +export async function sendEmailVerificationEmail(to: string, verifyUrl: string, locale: Locale, platform?: App.Platform): Promise { const safeUrl = safeAbsoluteUrl(verifyUrl); if (!safeUrl) { // 잘못된 URL 형식이면 메일 발송 자체 거부. console.error("[email] sendEmailVerificationEmail: 잘못된 verifyUrl scheme — 발송 취소"); return; } - const html = baseHtml( - "이메일 인증", - `

아래 버튼을 클릭하여 이메일 주소를 인증하세요. 링크는 24시간 동안 유효합니다.

-

- 이메일 인증 -

`, - ); - const text = `아래 링크에서 이메일 주소를 인증하세요. 링크는 24시간 동안 유효합니다.\n\n${safeUrl}\n\n본인이 요청하지 않았다면 이 이메일을 무시해 주세요.`; - await send(to, "이메일 인증 안내", html, text, platform); + const footer = translate(locale, "email.footer"); + const body = translate(locale, "email.verify.body"); + const html = baseHtml(locale, translate(locale, "email.verify.title"), ctaBody(body, safeUrl, translate(locale, "email.verify.button")), footer); + const text = `${translate(locale, "email.verify.text")}\n\n${safeUrl}\n\n${footer}`; + await send(to, translate(locale, "email.verify.subject"), html, text, platform); } -export async function sendInviteEmail(to: string, inviteUrl: string, platform?: App.Platform): Promise { +// F3: 이메일 변경 확인 메일 — 새 주소로 발송하는 확인 링크. verify 와 별개 라우트/문구를 쓴다. +export async function sendEmailChangeVerificationEmail(to: string, confirmUrl: string, locale: Locale, platform?: App.Platform): Promise { + const safeUrl = safeAbsoluteUrl(confirmUrl); + if (!safeUrl) { + console.error("[email] sendEmailChangeVerificationEmail: 잘못된 confirmUrl scheme — 발송 취소"); + return; + } + const footer = translate(locale, "email.footer"); + const body = translate(locale, "email.email_change.body"); + const html = baseHtml(locale, translate(locale, "email.email_change.title"), ctaBody(body, safeUrl, translate(locale, "email.email_change.button")), footer); + const text = `${translate(locale, "email.email_change.text")}\n\n${safeUrl}\n\n${footer}`; + await send(to, translate(locale, "email.email_change.subject"), html, text, platform); +} + +export async function sendInviteEmail(to: string, inviteUrl: string, locale: Locale, platform?: App.Platform): Promise { const safeUrl = safeAbsoluteUrl(inviteUrl); if (!safeUrl) { // 잘못된 URL 형식이면 메일 발송 자체 거부. console.error("[email] sendInviteEmail: 잘못된 inviteUrl scheme — 발송 취소"); return; } - const html = baseHtml( - "계정 초대", - `

계정에 초대되었습니다. 아래 버튼을 클릭하여 비밀번호를 설정하고 가입을 완료하세요. 링크는 72시간 동안 유효합니다.

-

- 초대 수락하기 -

`, - ); - const text = `계정에 초대되었습니다. 아래 링크에서 비밀번호를 설정하고 가입을 완료하세요. 링크는 72시간 동안 유효합니다.\n\n${safeUrl}\n\n본인이 요청하지 않았다면 이 이메일을 무시해 주세요.`; - await send(to, "계정 초대 안내", html, text, platform); + const footer = translate(locale, "email.footer"); + const body = translate(locale, "email.invite.body"); + const html = baseHtml(locale, translate(locale, "email.invite.title"), ctaBody(body, safeUrl, translate(locale, "email.invite.button")), footer); + const text = `${translate(locale, "email.invite.text")}\n\n${safeUrl}\n\n${footer}`; + await send(to, translate(locale, "email.invite.subject"), html, text, platform); } // ── 보안 알림 메일 (best-effort) ───────────────────────────────────────────── diff --git a/src/lib/server/security-notify.ts b/src/lib/server/security-notify.ts index 2f2426a..321ea9b 100644 --- a/src/lib/server/security-notify.ts +++ b/src/lib/server/security-notify.ts @@ -1,4 +1,4 @@ -import type { Locale } from "$lib/i18n/core"; +import { normalizeLocale } from "$lib/i18n/core"; import { translate } from "$lib/i18n/server"; import { sendSecurityAlertEmail } from "$lib/server/email"; @@ -11,15 +11,18 @@ export type SecurityEventKind = | "mfa_enrolled" | "mfa_disabled" | "backup_codes_regenerated" + | "backup_codes_low" + | "backup_codes_depleted" | "passkey_added" | "passkey_removed" - | "account_deletion_requested"; + | "account_deletion_requested" + | "email_change_requested" + | "session_revoked" + | "sessions_revoked_all"; -// users.locale ("ko-KR"/"en-US"/"ja-JP" 등) → i18n Locale. en* 만 en, 그 외(미상 포함)는 ko 기본. -// 근거: 지원 Locale 은 ko|en 뿐이며(core.ts), 알림 미발송보다 ko 기본 발송이 안전. -export function toLocale(userLocale: string | null | undefined): Locale { - return (userLocale ?? "").toLowerCase().startsWith("en") ? "en" : "ko"; -} +// users.locale ("ko-KR"/"en-US"/"ja-JP" 등) → i18n Locale. 공용 normalizeLocale(core.ts) 로 정규화한다. +// 하위 호환 별칭 — 기존 호출부/테스트가 toLocale 을 참조한다. +export const toLocale = normalizeLocale; // UTC 기준 사람이 읽을 수 있는 타임스탬프(locale 무관, 시간대 모호성 제거). function formatWhen(when: Date): string { @@ -38,7 +41,7 @@ function formatWhen(when: Date): string { export function dispatchSecurityAlert(params: { to: string | null | undefined; locale: string | null | undefined; kind: SecurityEventKind; when?: Date; platform?: App.Platform }): void { const { to, kind, platform } = params; if (!to) return; // 이메일 없는 계정 스킵 - const L = toLocale(params.locale); + const L = normalizeLocale(params.locale); const when = params.when ?? new Date(); const content = { subject: translate(L, `security_alert.${kind}.subject`), diff --git a/src/routes/(auth)/find-id/+page.server.ts b/src/routes/(auth)/find-id/+page.server.ts index 710a8b7..825febe 100644 --- a/src/routes/(auth)/find-id/+page.server.ts +++ b/src/routes/(auth)/find-id/+page.server.ts @@ -5,6 +5,7 @@ import { resolveSkinHtml, replacePlaceholders, escapeHtml } from "$lib/server/sk import { requireDbContext } from "$lib/server/auth/guards"; import { users } from "$lib/server/db/schema"; import { sendFindIdEmail } from "$lib/server/email"; +import { normalizeLocale } from "$lib/i18n/core"; import { checkRateLimit } from "$lib/server/ratelimit"; import { getRequestMetadata } from "$lib/server/audit"; import { translate } from "$lib/i18n/server"; @@ -83,7 +84,7 @@ export const actions: Actions = { } const [user] = await db - .select({ username: users.username }) + .select({ username: users.username, locale: users.locale }) .from(users) .where(and(eq(users.tenantId, tenant.id), eq(users.email, email))) .limit(1); @@ -93,7 +94,7 @@ export const actions: Actions = { // 왕복을 돌면 응답 시간 차이로 존재 여부가 새어 나간다. 메일 발송을 응답 // 경로에서 분리해 (Workers: waitUntil, Node: fire-and-forget) 존재/비존재 // 응답 시간을 균일하게 맞춘다. 메일 발송은 best-effort 이므로 결과를 기다리지 않는다. - const sendPromise = sendFindIdEmail(email, user.username, event.platform).catch(() => { + const sendPromise = sendFindIdEmail(email, user.username, normalizeLocale(user.locale ?? locale), event.platform).catch(() => { // 메일 발송 실패는 조용히 무시 }); const wait = event.platform?.ctx?.waitUntil?.bind(event.platform.ctx); diff --git a/src/routes/(auth)/find-password/+page.server.ts b/src/routes/(auth)/find-password/+page.server.ts index f48e424..8427b9a 100644 --- a/src/routes/(auth)/find-password/+page.server.ts +++ b/src/routes/(auth)/find-password/+page.server.ts @@ -5,6 +5,7 @@ import { resolveSkinHtml, replacePlaceholders, escapeHtml } from "$lib/server/sk import { requireDbContext } from "$lib/server/auth/guards"; import { users, passwordResetTokens } from "$lib/server/db/schema"; import { sendPasswordResetEmail, generateToken } from "$lib/server/email"; +import { normalizeLocale } from "$lib/i18n/core"; import { checkRateLimit } from "$lib/server/ratelimit"; import { getRequestMetadata } from "$lib/server/audit"; import { env } from "$env/dynamic/private"; @@ -89,7 +90,7 @@ export const actions: Actions = { } const [user] = await db - .select({ id: users.id, email: users.email }) + .select({ id: users.id, email: users.email, locale: users.locale }) .from(users) .where(and(eq(users.tenantId, tenant.id), eq(users.email, email), eq(users.username, username))) .limit(1); @@ -120,7 +121,7 @@ export const actions: Actions = { const resetUrl = `${issuer}/reset-password?${resetParams.toString()}`; // ctrls C5(후속): SMTP 왕복을 응답 경로에서 분리해 타이밍 계정 열거를 차단한다. // (find-id 와 동일 패턴 — Workers: waitUntil, Node: fire-and-forget.) - const sendPromise = sendPasswordResetEmail(user.email, resetUrl, event.platform).catch(() => { + const sendPromise = sendPasswordResetEmail(user.email, resetUrl, normalizeLocale(user.locale ?? locale), event.platform).catch(() => { // 메일 발송 실패는 조용히 무시 }); const wait = event.platform?.ctx?.waitUntil?.bind(event.platform.ctx); diff --git a/src/routes/(auth)/signup/+page.server.ts b/src/routes/(auth)/signup/+page.server.ts index b3519c5..1a24c6c 100644 --- a/src/routes/(auth)/signup/+page.server.ts +++ b/src/routes/(auth)/signup/+page.server.ts @@ -111,7 +111,7 @@ export const actions: Actions = { await db.insert(identities).values({ tenantId: tenant.id, userId, provider: "local", subject: email, email, linkedAt: now }); // 이메일 인증 메일 발송 — 실패해도 가입은 성공 처리(격리). - await issueEmailVerification(db, userId, email, event.platform); + await issueEmailVerification(db, userId, email, locale, event.platform); const redirectTo = sanitizeRedirectTarget(event.url.searchParams.get("redirectTo")); const skinHint = event.url.searchParams.get("skinHint") ?? ""; diff --git a/src/routes/account/confirm-email-change/+page.server.ts b/src/routes/account/confirm-email-change/+page.server.ts new file mode 100644 index 0000000..bae1a80 --- /dev/null +++ b/src/routes/account/confirm-email-change/+page.server.ts @@ -0,0 +1,96 @@ +import { fail } from "@sveltejs/kit"; +import { and, eq, isNull, ne } from "drizzle-orm"; +import type { Actions, PageServerLoad } from "./$types"; +import { requireDbContext } from "$lib/server/auth/guards"; +import { users, emailChangeTokens } from "$lib/server/db/schema"; +import { hashToken } from "$lib/server/email"; +import { runAtomic } from "$lib/server/db/atomic"; +import { checkRateLimit } from "$lib/server/ratelimit"; +import { recordAuditEvent, getRequestMetadata } from "$lib/server/audit"; +import { translate } from "$lib/i18n/server"; + +// 토큰 상태를 tenant 범위에서 조회한다(read-only). 유효하면 record 반환, 아니면 null. +// targetEmail 은 토큰에 바인딩된 변경 대상 주소 — 확인 시 이 값으로만 email 을 교체한다. +async function lookupToken(db: App.Locals["db"], tenantId: string, token: string) { + if (!db) return null; + const tokenHash = await hashToken(token); + const now = new Date(); + const [record] = await db + .select({ + tokenId: emailChangeTokens.id, + userId: users.id, + targetEmail: emailChangeTokens.targetEmail, + expiresAt: emailChangeTokens.expiresAt, + }) + .from(emailChangeTokens) + .innerJoin(users, eq(emailChangeTokens.userId, users.id)) + .where(and(eq(emailChangeTokens.tokenHash, tokenHash), isNull(emailChangeTokens.usedAt), eq(users.tenantId, tenantId))) + .limit(1); + if (!record || record.expiresAt < now) return null; + return record; +} + +export const load: PageServerLoad = async ({ locals, url }) => { + const token = url.searchParams.get("token"); + if (!token || !locals.db || !locals.tenant) { + return { valid: false, token: null as string | null }; + } + const record = await lookupToken(locals.db, locals.tenant.id, token); + if (!record) return { valid: false, token: null as string | null }; + return { valid: true, token: token as string | null }; +}; + +export const actions: Actions = { + default: async (event) => { + const { db, tenant, rateLimitStore } = requireDbContext(event.locals); + const locale = event.locals.locale; + + const formData = await event.request.formData(); + const token = String(formData.get("token") ?? ""); + + // 토큰 제출 브루트포스 방어 — 형제 인증 라우트와 동일하게 IP 당 제한. + const meta = getRequestMetadata(event); + const rl = await checkRateLimit(rateLimitStore, `confirm-email-change:${meta.ipKey}`, { windowMs: 15 * 60 * 1000, limit: 10 }); + if (!rl.allowed) { + return fail(429, { error: translate(locale, "errors.rate_limit", { minutes: Math.ceil(rl.retryAfterMs / 60000) }) }); + } + + if (!token) return fail(400, { error: translate(locale, "confirm_email_change.invalid_link") }); + + const record = await lookupToken(db, tenant.id, token); + if (!record) return fail(400, { error: translate(locale, "confirm_email_change.invalid_link") }); + + // 확인 시점 중복 재검사 — 요청 이후 다른 계정이 같은 주소를 선점했을 수 있다. + // (users_tenant_email_uidx 가 최종 방어이지만 사용자 친화적 에러를 위해 선검사한다.) + const [taken] = await db + .select({ id: users.id }) + .from(users) + .where(and(eq(users.tenantId, tenant.id), eq(users.email, record.targetEmail), ne(users.id, record.userId))) + .limit(1); + if (taken) return fail(409, { error: translate(locale, "confirm_email_change.invalid_link") }); + + const now = new Date(); + // 1회용 소진 + email 교체 + pending 클리어 + emailVerifiedAt 세팅을 원자적으로. + // 같은 user 의 미사용 변경 토큰을 모두 소진해 재사용을 차단한다. + await runAtomic(db, [ + (h) => + h + .update(emailChangeTokens) + .set({ usedAt: now }) + .where(and(eq(emailChangeTokens.userId, record.userId), isNull(emailChangeTokens.usedAt))), + (h) => h.update(users).set({ email: record.targetEmail, pendingEmail: null, pendingEmailRequestedAt: null, emailVerifiedAt: now, updatedAt: now }).where(eq(users.id, record.userId)), + ]); + + await recordAuditEvent(db, { + tenantId: tenant.id, + userId: record.userId, + actorId: record.userId, + kind: "email_changed", + outcome: "success", + ip: meta.ip, + userAgent: meta.userAgent, + }); + + return { changed: true }; + }, +}; diff --git a/src/routes/account/confirm-email-change/+page.svelte b/src/routes/account/confirm-email-change/+page.svelte new file mode 100644 index 0000000..f7eae51 --- /dev/null +++ b/src/routes/account/confirm-email-change/+page.svelte @@ -0,0 +1,49 @@ + + +
+
+
+
+

{t("confirm_email_change.title")}

+

{t("confirm_email_change.subtitle")}

+
+ + {#if changed} +
+ {t("confirm_email_change.success")} +
+ + + {t("confirm_email_change.go_profile")} → + + {:else if data.valid} + {#if err} +
{err}
+ {/if} +

{t("confirm_email_change.confirm_prompt")}

+
+ + +
+ {:else} +
+ {t("confirm_email_change.invalid_link")} +
+ + + {t("confirm_email_change.go_login")} → + + {/if} +
+
diff --git a/src/routes/account/profile/+page.server.ts b/src/routes/account/profile/+page.server.ts index 7357036..2c51b1a 100644 --- a/src/routes/account/profile/+page.server.ts +++ b/src/routes/account/profile/+page.server.ts @@ -1,12 +1,17 @@ import { fail, redirect } from "@sveltejs/kit"; -import { and, eq } from "drizzle-orm"; +import { and, eq, ne } from "drizzle-orm"; import type { Actions, PageServerLoad } from "./$types"; import { requireDbContext } from "$lib/server/auth/guards"; import { users } from "$lib/server/db/schema"; import { getUserMembership } from "$lib/server/org/membership"; import { issueEmailVerification } from "$lib/server/auth/email-verification"; +import { issueEmailChange } from "$lib/server/auth/email-change"; +import { findPasswordCredential } from "$lib/server/auth/users"; +import { verifyPassword } from "$lib/server/auth/password"; +import { dispatchSecurityAlert } from "$lib/server/security-notify"; import { checkRateLimit } from "$lib/server/ratelimit"; import { translate } from "$lib/i18n/server"; +import { normalizeLocale } from "$lib/i18n/core"; export const load: PageServerLoad = async ({ locals }) => { if (!locals.user) throw redirect(303, "/login"); @@ -28,6 +33,8 @@ export const load: PageServerLoad = async ({ locals }) => { }, email: locals.user.email, emailVerified: !!locals.user.emailVerifiedAt, + // F3: 확인 대기 중인 새 이메일(있으면). UI 통합은 별도(profile.svelte 는 다른 에이전트 범위). + pendingEmail: locals.user.pendingEmail, membership, }; }; @@ -49,7 +56,7 @@ export const actions: Actions = { // birthdate 형식 검증 (YYYY-MM-DD) if (birthdate && !/^\d{4}-\d{2}-\d{2}$/.test(birthdate)) { - return fail(400, { error: "생년월일 형식이 올바르지 않습니다. (YYYY-MM-DD)" }); + return fail(400, { error: translate(locals.locale, "profile.err_birthdate_format") }); } await db @@ -87,7 +94,72 @@ export const actions: Actions = { return fail(429, { resendError: translate(locale, "errors.rate_limit", { minutes: Math.ceil(rl.retryAfterMs / 60000) }) }); } - await issueEmailVerification(db, locals.user.id, locals.user.email, event.platform); + // 수신자(=본인) locale 우선. users.locale 없으면 요청 locale 로 폴백. + await issueEmailVerification(db, locals.user.id, locals.user.email, normalizeLocale(locals.user.locale ?? locale), event.platform); return { resent: true }; }, + + // F3: 이메일 주소 변경 요청. 현 비밀번호 재인증 → 형식·중복 검증 → pendingEmail 저장 + + // 변경 토큰 발급(새 주소로 확인 메일) + 기존 주소로 "변경 시도" 보안 알림. 실제 교체는 + // confirm-email-change 라우트에서 토큰 확인 시 일어난다. + changeEmail: async (event) => { + const { locals } = event; + if (!locals.user) throw redirect(303, "/login"); + const { db, tenant, rateLimitStore } = requireDbContext(locals); + const locale = locals.locale; + const user = locals.user; + + const fd = await event.request.formData(); + const newEmail = String(fd.get("newEmail") ?? "") + .trim() + .toLowerCase(); + const password = String(fd.get("password") ?? ""); + + // 토큰 제출/재인증 브루트포스 방어 — 계정 단위 rate-limit. + const rl = await checkRateLimit(rateLimitStore, `change-email:${user.id}`, { windowMs: 60 * 60 * 1000, limit: 5 }); + if (!rl.allowed) { + return fail(429, { changeEmailError: translate(locale, "errors.rate_limit", { minutes: Math.ceil(rl.retryAfterMs / 60000) }) }); + } + + // ── 현 비밀번호 재인증(step-up) — 세션 탈취 공격자가 이메일을 바꿔치기하지 못하게. ── + const pwCred = await findPasswordCredential(db, user.id); + if (!pwCred?.secret) { + return fail(400, { changeEmailError: translate(locale, "profile.err_email_change_no_password") }); + } + const pw = await verifyPassword(password, pwCred.secret); + if (!pw.valid) { + return fail(400, { changeEmailError: translate(locale, "profile.err_email_change_password") }); + } + + // ── 새 이메일 형식·동일·중복 검증(signup 과 동일 정규식). ── + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) { + return fail(400, { changeEmailError: translate(locale, "profile.err_email_change_format") }); + } + if (newEmail === user.email.toLowerCase()) { + return fail(400, { changeEmailError: translate(locale, "profile.err_email_change_same") }); + } + const [taken] = await db + .select({ id: users.id }) + .from(users) + .where(and(eq(users.tenantId, tenant.id), eq(users.email, newEmail), ne(users.id, user.id))) + .limit(1); + if (taken) { + return fail(409, { changeEmailError: translate(locale, "profile.err_email_change_taken") }); + } + + const now = new Date(); + // pendingEmail 대기 상태 기록(감사/UI 용). 실제 email 교체는 confirm 시점. + await db + .update(users) + .set({ pendingEmail: newEmail, pendingEmailRequestedAt: now, updatedAt: now }) + .where(and(eq(users.id, user.id), eq(users.tenantId, tenant.id))); + + // 새 주소로 확인 링크 메일(수신자 locale 우선, best-effort 격리). + await issueEmailChange(db, user.id, newEmail, normalizeLocale(user.locale ?? locale), event.platform); + + // 기존 주소로 "변경 시도됨" 보안 알림(best-effort). 탈취 시 정당 소유자가 인지·차단하도록. + dispatchSecurityAlert({ to: user.email, locale: user.locale, kind: "email_change_requested", when: now, platform: event.platform }); + + return { changeEmailSent: true, pendingEmail: newEmail }; + }, }; diff --git a/src/routes/account/profile/+page.svelte b/src/routes/account/profile/+page.svelte index 2f41c40..c45bc82 100644 --- a/src/routes/account/profile/+page.svelte +++ b/src/routes/account/profile/+page.svelte @@ -1,7 +1,9 @@
@@ -112,4 +134,32 @@ let editingId = $state(null);
+ + +
+
+

{t("oidc.org_claims.title")}

+
+

{t("oidc.org_claims.desc")}

+ + {#if (form as { organizationClaimsUpdated?: boolean } | null)?.organizationClaimsUpdated} +
{t("oidc.org_claims.saved")}
+ {/if} + +
+ {#each ORG_CLAIM_FIELDS as field (field)} + + {/each} +
+

{t("oidc.org_claims.all_note")}

+ +
+
+
diff --git a/src/routes/oidc/token/+server.ts b/src/routes/oidc/token/+server.ts index 8914a54..1a921c8 100644 --- a/src/routes/oidc/token/+server.ts +++ b/src/routes/oidc/token/+server.ts @@ -7,7 +7,7 @@ import { findActiveUserById } from "$lib/server/auth/users"; import { recordAuditEvent, getRequestMetadata } from "$lib/server/audit"; import { checkRateLimit } from "$lib/server/ratelimit"; import { findOidcClient, isValidClientSecret, parseBasicAuth } from "$lib/server/oidc/client"; -import { buildAddressClaim } from "$lib/server/oidc/claims"; +import { buildAddressClaim, buildOrganizationClaims, parseOrganizationClaimConfig } from "$lib/server/oidc/claims"; import { findAndConsumeGrant } from "$lib/server/oidc/grant"; import { verifyPkce } from "$lib/server/oidc/pkce"; import { issueRefreshToken, rotateRefreshToken, revokeRefreshTokenFamily } from "$lib/server/oidc/refresh"; @@ -73,6 +73,7 @@ interface BuildTokenParams { user: NonNullable>>; clientId: string; // 공개 client_id (aud/azp/access token 용) clientDbId: string; // oidcClients.id PK (서비스 권한 조회용) + organizationClaimConfig: string | null; // oidcClients.organizationClaimConfig(JSON text). null=전량 노출. scope: string; sessionId: string | null; nonce: string | null; @@ -145,11 +146,17 @@ async function buildTokens(params: BuildTokenParams): Promise<{ idToken: string; } } - // groups scope — 활성 조직 멤버십을 code(없으면 name) 문자열 배열로 매핑. - // userinfo 응답과 동일 로직(membershipToGroups)을 공유한다. 표준 claim 이므로 assignment 머지 이후에 설정. - if (scopes.has("groups")) { + // groups / organization scope — 활성 조직 멤버십 기반. userinfo 응답과 동일 로직 + // (membershipToGroups / buildOrganizationClaims)을 공유한다. 표준 claim 이므로 assignment + // 머지 이후에 설정. 두 scope 모두 요청돼도 멤버십은 한 번만 조회한다. + if (scopes.has("groups") || scopes.has("organization")) { const membership = await getUserMembership(db, user.id); - idTokenPayload.groups = membershipToGroups(membership); + if (scopes.has("groups")) { + idTokenPayload.groups = membershipToGroups(membership); + } + if (scopes.has("organization")) { + Object.assign(idTokenPayload, buildOrganizationClaims(membership, parseOrganizationClaimConfig(params.organizationClaimConfig))); + } } const idToken = await signJwt(idTokenPayload, signingKey.privateKey, signingKey.kid); @@ -372,6 +379,7 @@ export const POST: RequestHandler = async (event) => { user, clientId, clientDbId: client.id, + organizationClaimConfig: client.organizationClaimConfig, scope: effectiveScope, sessionId: record.sessionId, nonce: null, // refresh 로 재발급되는 id_token 에는 nonce 를 넣지 않는다 (원 요청 전용) @@ -464,6 +472,7 @@ export const POST: RequestHandler = async (event) => { user, clientId, clientDbId: client.id, + organizationClaimConfig: client.organizationClaimConfig, scope: grant.scope, sessionId: grant.sessionId, nonce: grant.nonce, diff --git a/src/routes/oidc/userinfo/+server.ts b/src/routes/oidc/userinfo/+server.ts index 9a1128b..a051bc4 100644 --- a/src/routes/oidc/userinfo/+server.ts +++ b/src/routes/oidc/userinfo/+server.ts @@ -6,7 +6,7 @@ import { oidcClients, users } from "$lib/server/db/schema"; import { verifyAccessToken, tryWithSecretsNullable } from "$lib/server/crypto/keys"; import { getUserMembership, membershipToGroups } from "$lib/server/org/membership"; import { getActiveAssignment, parseAssignmentAttributes } from "$lib/server/access/service-permissions"; -import { buildAddressClaim } from "$lib/server/oidc/claims"; +import { buildAddressClaim, buildOrganizationClaims, parseOrganizationClaimConfig } from "$lib/server/oidc/claims"; import { translate } from "$lib/i18n/server"; const RESERVED_USERINFO_CLAIMS = new Set(["sub", "iss", "aud", "iat", "exp", "auth_time"]); @@ -48,7 +48,7 @@ async function handleUserinfo(locals: App.Locals, request: Request): Promise ({ - id: d.id, - name: d.name, - code: d.code, - is_primary: d.isPrimary, - job_title: d.jobTitle, - position: d.position - ? { - id: d.position.id, - name: d.position.name, - code: d.position.code, - level: d.position.level, - } - : null, - })); - response.team = membership.teams.map((t) => ({ - id: t.id, - name: t.name, - code: t.code, - department: t.departmentName, - is_primary: t.isPrimary, - job_title: t.jobTitle, - })); - response.position = membership.primaryPosition?.name ?? null; - response.job_title = membership.primaryJobTitle ?? null; + Object.assign(response, buildOrganizationClaims(membership, parseOrganizationClaimConfig(issuingClient.organizationClaimConfig))); } } diff --git a/test/unit/oidc-claims.test.ts b/test/unit/oidc-claims.test.ts new file mode 100644 index 0000000..c434e84 --- /dev/null +++ b/test/unit/oidc-claims.test.ts @@ -0,0 +1,186 @@ +import { describe, it, expect } from "vitest"; +import { buildOrganizationClaims, parseOrganizationClaimConfig } from "$lib/server/oidc/claims"; +import type { UserMembership } from "$lib/server/org/membership"; + +/** 클레임 노출 검증용 고정 멤버십(주소속 부서 1 + 팀 1). */ +function fixtureMembership(): UserMembership { + return { + departments: [ + { + id: "dept-1", + name: "플랫폼실", + code: "PLTF", + isPrimary: true, + jobTitle: "리드", + position: { id: "pos-1", name: "책임", code: "L3", level: 3 }, + }, + ], + teams: [ + { + id: "team-1", + name: "인증팀", + code: "AUTH", + departmentName: "플랫폼실", + isPrimary: true, + jobTitle: "팀원", + }, + ], + parts: [], + primaryPosition: { id: "pos-1", name: "책임", code: "L3", level: 3 }, + primaryJobTitle: "리드", + }; +} + +/** 비어있는 멤버십(소속 없음). */ +function emptyMembership(): UserMembership { + return { + departments: [], + teams: [], + parts: [], + primaryPosition: null, + primaryJobTitle: null, + }; +} + +describe("buildOrganizationClaims", () => { + it("config null → 4필드(department/team/position/job_title) 전량 노출", () => { + const claims = buildOrganizationClaims(fixtureMembership(), null); + + expect(Object.keys(claims).sort()).toEqual(["department", "job_title", "position", "team"]); + expect(claims.department).toEqual([ + { + id: "dept-1", + name: "플랫폼실", + code: "PLTF", + is_primary: true, + job_title: "리드", + position: { id: "pos-1", name: "책임", code: "L3", level: 3 }, + }, + ]); + expect(claims.team).toEqual([ + { + id: "team-1", + name: "인증팀", + code: "AUTH", + department: "플랫폼실", + is_primary: true, + job_title: "팀원", + }, + ]); + expect(claims.position).toBe("책임"); + expect(claims.job_title).toBe("리드"); + }); + + it("config undefined → null 과 동일하게 전량 노출(하위호환)", () => { + const claims = buildOrganizationClaims(fixtureMembership()); + expect(Object.keys(claims).sort()).toEqual(["department", "job_title", "position", "team"]); + }); + + it("{ team: false } → team 키만 생략, 나머지 3필드 노출", () => { + const claims = buildOrganizationClaims(fixtureMembership(), { team: false }); + + expect("team" in claims).toBe(false); + expect(Object.keys(claims).sort()).toEqual(["department", "job_title", "position"]); + expect(claims.department).toHaveLength(1); + expect(claims.position).toBe("책임"); + expect(claims.job_title).toBe("리드"); + }); + + it("{ position: false, jobTitle: false } → 두 최상위 키 생략, department/team 노출", () => { + const claims = buildOrganizationClaims(fixtureMembership(), { position: false, jobTitle: false }); + + expect("position" in claims).toBe(false); + expect("job_title" in claims).toBe(false); + expect(Object.keys(claims).sort()).toEqual(["department", "team"]); + expect(claims.department).toHaveLength(1); + expect(claims.team).toHaveLength(1); + }); + + it("{ department: false } → department 키만 생략(jobTitle 토글과 job_title 은 독립)", () => { + const claims = buildOrganizationClaims(fixtureMembership(), { department: false }); + + expect("department" in claims).toBe(false); + expect(Object.keys(claims).sort()).toEqual(["job_title", "position", "team"]); + // jobTitle 토글은 최상위 job_title 클레임만 제어하며 department.job_title 과 무관. + expect(claims.job_title).toBe("리드"); + }); + + it("전부 false → 조직 클레임 없음(빈 객체)", () => { + const claims = buildOrganizationClaims(fixtureMembership(), { + department: false, + team: false, + position: false, + jobTitle: false, + }); + + expect(claims).toEqual({}); + expect(Object.keys(claims)).toHaveLength(0); + }); + + it("true 명시는 노출(false 만 생략을 유발)", () => { + const claims = buildOrganizationClaims(fixtureMembership(), { team: true, position: false }); + + expect("team" in claims).toBe(true); + expect("position" in claims).toBe(false); + }); + + it("멤버십이 비어있으면 노출 필드는 빈 배열/null 로 그대로 나온다", () => { + const claims = buildOrganizationClaims(emptyMembership(), null); + + expect(claims.department).toEqual([]); + expect(claims.team).toEqual([]); + expect(claims.position).toBeNull(); + expect(claims.job_title).toBeNull(); + }); + + it("멤버십이 비어있어도 config off 는 해당 키를 생략한다", () => { + const claims = buildOrganizationClaims(emptyMembership(), { department: false, team: false }); + + expect(Object.keys(claims).sort()).toEqual(["job_title", "position"]); + expect(claims.position).toBeNull(); + expect(claims.job_title).toBeNull(); + }); +}); + +describe("parseOrganizationClaimConfig", () => { + it("null/undefined/빈 문자열 → null(전량 노출 폴백)", () => { + expect(parseOrganizationClaimConfig(null)).toBeNull(); + expect(parseOrganizationClaimConfig(undefined)).toBeNull(); + expect(parseOrganizationClaimConfig("")).toBeNull(); + }); + + it("유효 JSON → 알려진 boolean 필드만 취한다", () => { + expect(parseOrganizationClaimConfig('{"team":false}')).toEqual({ team: false }); + expect(parseOrganizationClaimConfig('{"department":true,"team":false,"position":true,"jobTitle":false}')).toEqual({ + department: true, + team: false, + position: true, + jobTitle: false, + }); + }); + + it("boolean 이 아닌 값/미지 키는 무시한다(오염 방지)", () => { + expect(parseOrganizationClaimConfig('{"team":"nope","position":1,"jobTitle":false,"evil":true}')).toEqual({ + jobTitle: false, + }); + }); + + it("잘못된 JSON → null(안전 폴백 = 전량 노출)", () => { + expect(parseOrganizationClaimConfig("{not json}")).toBeNull(); + expect(parseOrganizationClaimConfig("undefined")).toBeNull(); + }); + + it("객체가 아닌 유효 JSON(배열/스칼라) → null", () => { + expect(parseOrganizationClaimConfig("null")).toBeNull(); + expect(parseOrganizationClaimConfig("42")).toBeNull(); + // 배열은 typeof object 이지만 알려진 필드가 없어 빈 config 로 정규화된다. + expect(parseOrganizationClaimConfig("[1,2,3]")).toEqual({}); + }); + + it("파싱 결과를 buildOrganizationClaims 에 넘기면 off 경로가 적용된다(왕복)", () => { + const config = parseOrganizationClaimConfig('{"position":false,"jobTitle":false}'); + const claims = buildOrganizationClaims(fixtureMembership(), config); + + expect(Object.keys(claims).sort()).toEqual(["department", "team"]); + }); +}); From 1368e4fab9f986cf715778f6f9c31583bca68400 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 17:45:34 +0900 Subject: [PATCH 24/26] =?UTF-8?q?docs(admin):=20=EC=9A=B4=EC=98=81=20?= =?UTF-8?q?=EB=A7=A4=EB=89=B4=EC=96=BC=C2=B7=EC=8A=A4=ED=82=A8=20placehold?= =?UTF-8?q?er=206=EC=A2=85=C2=B7=EB=B0=B1=EC=97=85=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=80=EC=9E=94=EB=9F=89=20=EA=B2=BD=EA=B3=A0=20(P17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/ADMIN_GUIDE.md 신설: 조직(dept/team/part/position) 관리, OIDC/SAML 클라이언트 등록, scope/role, 스킨 placeholder 6종, 서명키 회전, 감사로그 - admin/skins·skins/guide: placeholder 문서에 IDP_REGISTERED·IDP_PASSWORD_RESET·IDP_FLASH_MSG 3종 보강(실제값 정합) - mfa: 백업코드 통과 시 잔량 계산 → 저잔량(≤2) 경고·소진(0) 알림 메일(D4, TOTP 통과 시 미검사로 오탐 방지). rate-limit hunk(P15)와 분리해 백업 hunk 만. Co-Authored-By: Claude Fable 5 --- docs/ADMIN_GUIDE.md | 285 ++++++++++++++++++++++ src/routes/(auth)/mfa/+page.server.ts | 20 ++ src/routes/admin/skins/+page.svelte | 3 + src/routes/admin/skins/guide/+page.svelte | 2 +- 4 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 docs/ADMIN_GUIDE.md diff --git a/docs/ADMIN_GUIDE.md b/docs/ADMIN_GUIDE.md new file mode 100644 index 0000000..4d53b4d --- /dev/null +++ b/docs/ADMIN_GUIDE.md @@ -0,0 +1,285 @@ +# KeyStone 관리자 운영 매뉴얼 + +KeyStone(멀티테넌트 IdP)의 관리 콘솔 운영 가이드입니다. 각 화면에서 관리자가 무엇을 클릭하고 어떤 일이 일어나는지를 실무 관점에서 정리했습니다. + +> 콘솔 UI 는 한국어/영어(ko/en)를 지원합니다. 모든 관리 작업은 현재 로그인한 테넌트 범위에서만 동작하며, 주요 변경은 감사 로그(`/admin/audit`)에 기록됩니다. + +--- + +## 1. 개요 / 접근 + +### 관리자 로그인 + +- 로그인 URL: **`/admin/login`** +- 인증 흐름(`/admin/login` → `/mfa`): + 1. 아이디/비밀번호 검증(로컬 계정). + 2. **`role === "admin"` 이 아니면 거부**(감사 로그에 `reason: not_admin`). + 3. **TOTP(MFA) 미등록 관리자는 로그인 불가**(`reason: mfa_not_configured`) — 관리자는 반드시 TOTP 를 등록해야 합니다. + 4. MFA pending 쿠키 발급 후 `/mfa` 로 이동해 TOTP 코드 확인 → 세션 생성. +- 레이트리밋: IP당 15분에 10회. +- `IDP_SIGNING_KEY_SECRET` 미설정 시 MFA 토큰 서명이 불가해 로그인이 503 으로 막힙니다. + +### 접근 제어 + +- `/admin/**` 전 구간은 레이아웃 가드(`+layout.server.ts`)가 보호합니다. + - 미로그인 → `/admin/login?redirectTo=...` 로 리다이렉트. + - **`role !== "admin"` → `/` 로 강제 이동**(일반 사용자는 콘솔 접근 불가). +- `/admin/login` 만 예외적으로 비인증 접근 허용. + +### 대시보드 + +- **`/admin`**: 테넌트 요약 카운트(사용자, OIDC 클라이언트, SAML SP, 서명키, 감사 이벤트, 부서/팀/직급) 표시. + +--- + +## 2. 조직 관리 (부서 / 팀 / 파트 / 직급) + +조직은 **부서(department) → 팀(team) → 파트(part)** 3단계 계층이며, **직급(position)** 은 별도 축입니다. + +| 화면 | 경로 | 상위 참조 | 계층 | +| ---- | -------------------- | ------------------------------------- | ------------- | +| 부서 | `/admin/departments` | 상위 부서(`parentId`, 자기 참조 트리) | 최상위 | +| 팀 | `/admin/teams` | 부서(`departmentId`) | 부서 하위 | +| 파트 | `/admin/parts` | 팀(`teamId`) | 팀 하위 | +| 직급 | `/admin/positions` | 없음 | 독립(레벨 축) | + +### 부서 (`/admin/departments`) + +- 필드: 이름(필수), 코드, 상위 부서, 설명, 표시순서(`displayOrder`, 빈값 0). 수정 시 상태(active/inactive 등) 지정. +- **부서 트리 검증**(등록·수정 공통): + - 최대 깊이 **8단계**. + - 자기 자신을 상위로 지정 불가. + - 상위 체인에 순환 참조가 생기면 차단(간접 순환 A→B→A 포함). + - 상위 부서 선택지는 **활성(active) 부서**만 노출. +- 부서 트리 변경은 권한 상속에 직결되므로 **모든 변경이 감사 로그**(`department_*`)에 기록됩니다. + +### 팀 (`/admin/teams`) + +- 필드: 이름(필수), 코드, 소속 부서, 설명. 수정 시 상태. +- 소속 부서 선택지는 **활성 부서**만. 지정한 부서가 같은 테넌트에 존재하는지 참조 무결성 검증. + +### 파트 (`/admin/parts`) + +- 필드: 이름(필수), 코드, 소속 팀, 설명. 수정 시 상태. +- 소속 팀 선택지는 **활성 팀**만(부서명 병기). 참조 무결성 검증. + +### 직급 (`/admin/positions`) + +- 필드: 이름(필수), 코드, **레벨(`level`, 정수)**. 레벨 오름차순으로 정렬 표시. + +### 사용자 소속 배정 & 주소속(primary) 의미 + +개별 사용자의 소속은 **`/admin/users/[id]`** 상세 화면에서 배정합니다(부서/팀/파트 각각 add/remove). + +- 배정 시 각 소속에 **직책(jobTitle)** 을 지정할 수 있고, 부서 배정에는 **직급(position)** 을 함께 지정합니다. +- **주소속(primary)**: 각 축(부서/팀/파트)마다 `isPrimary` 체크박스로 지정. 소속 해제는 하드 삭제가 아니라 **`endedAt` 설정(소프트 종료)** 으로 처리됩니다(이력 보존). +- **주소속 부서**의 직급/직책이 OIDC `organization` 클레임의 최상위 `position` / `job_title` 값이 됩니다(주소속이 없으면 현재 소속 목록의 첫 부서를 사용). + +--- + +## 3. OIDC 클라이언트 등록/관리 (`/admin/oidc-clients`) + +### 생성 + +- **client_id**: 자동 생성(무작위 20자). +- **client_secret**: `token_endpoint_auth_method` 가 `none`(public)이 아니면 자동 생성되어 **생성 직후 화면에 1회만 노출**됩니다. DB 에는 해시만 저장되므로 이때 반드시 복사해 두어야 합니다. +- **Redirect URIs**(필수): 줄바꿈/콤마로 여러 개. `https` 또는 loopback(`http://localhost` 등) 허용, 모바일용 **커스텀 스킴 허용**. `javascript:`/`data:`/`file:`/`blob:`/`vbscript:` 및 fragment(`#`) 포함 URI 는 거부. +- **Post-Logout Redirect URIs / Front-channel / Back-channel Logout URI**: 로그아웃 관련 URL(각 세션 요구 플래그 포함). 커스텀 스킴 불허(https/loopback 만). +- **token_endpoint_auth_method**: `client_secret_basic` / `client_secret_post` / `none` 중 선택. +- **PKCE(`requirePkce`)**: 체크로 강제. **public 클라이언트(`none`)는 PKCE 가 항상 강제**되며 수정 시에도 해제 불가. +- **Wildcard Redirect URI(`allowWildcardRedirectUri`)**: 보안상 기본 비활성. 와일드카드 매칭이 꼭 필요할 때만 **명시적 opt-in**(체크). +- **Scopes**(공백 구분): `openid`(필수) / `profile` / `email` / `address` / `phone` / `offline_access` / `organization` / `groups`. `openid` 누락 시 거부. + - `offline_access` 를 넣어야 refresh token(grant) 이 발급됩니다. + +### 관리 + +- **수정**: 이름/URI/scope/로그아웃 설정/PKCE/와일드카드/활성화(enabled) 변경. +- **시크릿 재발급(`regenerateSecret`)**: 새 시크릿 생성 후 **1회 노출**. 기존 시크릿은 즉시 무효화됩니다. +- **삭제**: 클라이언트 제거. +- 생성/수정/시크릿재발급/삭제 모두 감사 로그(`oidc_client_*`) 기록. 모든 폼은 CSRF 토큰으로 보호됩니다. + +--- + +## 4. 서비스 role/scope 설정 (`/admin/oidc-clients/[id]`) + +클라이언트 상세 화면에서 **서비스 role** 을 정의합니다(SAML SP 도 `/admin/saml-sps/[id]` 에서 동일 구조). + +- role 필드: + - **key**(필수): `^[A-Za-z0-9_.-]{1,64}$` 형식. 같은 서비스 내 중복 불가(중복 시 409). + - **label**(필수): 표시 이름. + - **description**: 설명(선택). + - **isDefault**: 기본 부여 role 표시. + - **displayOrder**: 정렬 순서(정수). +- role 추가/삭제는 감사 로그(`service_role_created` / `service_role_deleted`)에 기록됩니다. +- 정의한 role 은 `/admin/users/[id]` 에서 사용자에게 **서비스 권한(assignment)** 으로 부여합니다(만료/취소 관리 포함). + +--- + +## 5. organization 클레임 노출 설정 (`/admin/oidc-clients/[id]`) + +클라이언트 상세 화면 하단 **"조직 클레임 노출 설정"** 에서, `organization` scope 로 노출되는 조직 정보를 필드별로 on/off 합니다. + +### 노출되는 클레임 구조 + +`organization` scope 가 켜진 클라이언트의 **id_token 과 userinfo 응답에 동일하게** 아래 4개 최상위 키가 들어갑니다. + +| 클레임 키 | 내용 | +| ------------ | --------------------------------------------------------------------------------------------------------------------------- | +| `department` | 현재 소속 부서 배열. 각 원소: `id`, `name`, `code`, `is_primary`, `job_title`, `position`(`{id,name,code,level}` 또는 null) | +| `team` | 현재 소속 팀 배열. 각 원소: `id`, `name`, `code`, `department`(부서명), `is_primary`, `job_title` | +| `position` | 주소속 부서의 직급명(문자열) 또는 null | +| `job_title` | 주소속 부서의 직책(문자열) 또는 null | + +### 체크박스 4개와 저장 규칙 + +토글 필드는 **`department` / `team` / `position` / `jobTitle`** 4개입니다. + +- **모든 필드를 켜면 → `null`(미설정)로 저장**됩니다. 즉 "전량 노출"이며, DB 를 깨끗하게 유지하고 **기존 동작과 하위호환**을 보장합니다. +- **하나라도 끄면 → 명시적 JSON** 으로 저장됩니다. 예: + ```json + { "department": true, "team": true, "position": false, "jobTitle": true } + ``` + `false` 인 필드의 **최상위 클레임 키 자체가 응답에서 생략**됩니다. +- 저장 위치: `oidcClients.organizationClaimConfig`(JSON text). +- **id_token 과 userinfo 가 동일한 config 를 적용**하므로 두 응답의 조직 정보가 항상 일치합니다. + +### 하위호환 / 무회귀 + +- `organizationClaimConfig` 가 없는(=null) 기존 클라이언트는 **전량 노출**로 동작합니다. 이번 기능 도입으로 인한 기존 클라이언트 회귀는 없습니다. +- 저장값 파싱이 실패하거나 알 수 없는 값이면 안전하게 null(전량 노출)로 폴백합니다. +- 변경은 감사 로그(`oidc_client_updated`, `detail.organizationClaimConfig`)에 기록됩니다. + +--- + +## 6. SAML SP 등록/관리 (`/admin/saml-sps`) + +### 생성 / 수정 + +- 필드: **이름**(필수), **Entity ID**(필수, 테넌트 내 중복 불가 → 중복 시 409), **ACS URL**(필수), SLO URL, SP 인증서(`cert`), NameID Format. +- **ACS/SLO URL 검증**: `validateSamlUrl` 로 형식 검사. +- **NameID Format**: emailAddress / unspecified / persistent / transient(SAML 표준 URN) 중에서만 허용. +- 서명/암호화 옵션: + - **`signResponse` 는 항상 `true` 로 강제**됩니다(관리 UI 가 false 를 보내도 무시). XSW 계열 공격 방지를 위해 IdP 가 Response 자체를 항상 서명. + - `signAssertion`, `wantAuthnRequestsSigned` 는 토글. + - **`encryptAssertion` 을 켜려면 SP 공개키(cert)가 반드시 있어야** 합니다(없으면 400). +- **allowedAttributes**: 콤마 구분. 허용 키 화이트리스트(`email`, `username`, `displayName`, `givenName`, `familyName`, `surName`, `phoneNumber`, `department`, `team`, `jobTitle`, `position`, `Role`, `RoleLabel`)에 없는 값은 무시됩니다. +- 보안 설정 변경(특히 **cert / acsUrl / wantAuthnRequestsSigned**)은 ACS 하이재킹 포렌식을 위해 before/after diff 가 감사 로그(`saml_sp_updated`)에 상세 기록됩니다. + +### 상세 (`/admin/saml-sps/[id]`) + +- OIDC 클라이언트와 동일하게 **서비스 role** 을 정의(key/label/description/isDefault/displayOrder). 4장 참조. + +### 메타데이터 + +- IdP 측 SAML 메타데이터는 `/saml/metadata` 에서 제공됩니다(SP 설정 시 참조). + +--- + +## 7. 스킨(커스텀 로그인 UI) 등록 (`/admin/skins`) + +외부에 호스팅한 HTML 을 가져와 로그인/가입 등 인증 화면을 클라이언트별로 커스터마이즈합니다. 사용법 안내는 **`/admin/skins/guide`** 에서 확인할 수 있습니다. + +### 등록 필드 + +- **대상 클라이언트**: `clientType`(oidc/saml) + `clientRefId`. +- **스킨 타입(`skinType`)**: `login` / `signup` / `find_id` / `find_password` / `mfa` / `reset_password`. +- **Fetch URL**: 스킨 HTML 을 가져올 URL. **https 필수**, loopback/내부주소(127.x, link-local) 금지(SSRF 방지). +- **Fetch Secret**: 스킨 서버 인증용 시크릿. IdP 가 스킨 HTML 을 가져올 때 **`X-IDP-Token`** 헤더로 이 값을 전송하므로, 스킨 서버는 이 헤더를 검증해 접근을 통제할 수 있습니다(선택). +- **캐시 TTL(`cacheTtlSeconds`)**: 기본 3600초, 0 이상, **최대 86400초(1일)**. + +### 운영 + +- **수정 / 삭제 / 활성화 토글 / 캐시 무효화(`invalidateCache`)** 지원. URL·TTL 변경이나 삭제 시 캐시가 자동 무효화됩니다. +- 같은 (클라이언트, 스킨타입) 조합 중복 등록 시 409. + +### 치환자(placeholder) + +스킨 HTML 안에서 `{{...}}` 형태로 사용하며, IdP 가 렌더링 시 값을 채웁니다. **총 6개**이고, 스킨 타입별 적용 범위가 다릅니다. + +| 치환자 | 채워지는 값 | 적용 스킨 | +| ------------------------ | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| `{{IDP_FORM_ACTION}}` | 항상 빈 문자열 `""`(비어 있으면 폼이 **현재 URL로 POST**) | 모든 스킨 공통 | +| `{{IDP_REDIRECT_TO}}` | `escapeHtml(redirectTo ?? "")` — hidden input 용 | **login / signup / reset_password** 에서 채워짐. find_id·find_password·mfa 는 `""` | +| `{{IDP_SKIN_HINT}}` | `escapeHtml(skinHint)` — hidden input 용(어떤 스킨을 쓸지 서버에 되전달) | 모든 스킨 공통 | +| `{{IDP_REGISTERED}}` | 회원가입 완료 직후 `"1"`, 그 외 `""`(가입 완료 안내 노출용) | **login 전용** | +| `{{IDP_PASSWORD_RESET}}` | 비밀번호 재설정 완료 직후 `"1"`, 그 외 `""`(재설정 완료 안내 노출용) | **login 전용** | +| `{{IDP_FLASH_MSG}}` | `escapeHtml(flashMsg)` — 서버가 채우는 플래시/오류 메시지(이미 HTML 이스케이프됨). 없으면 `""` | 모든 스킨 공통(폼 재제출 오류 표시) | + +> **필수 hidden input**: `login` 스킨의 `
` 에는 최소한 `redirectTo`(값 `{{IDP_REDIRECT_TO}}`)와 `skinHint`(값 `{{IDP_SKIN_HINT}}`) hidden input 및 `username`/`password` 입력이 있어야 정상 동작합니다. 폼 `action` 은 `{{IDP_FORM_ACTION}}`(빈 값=현재 URL POST)으로 둡니다. + +### 캐시 동작 + +- 가져온 스킨 HTML 은 TTL 동안 캐시됩니다. 스킨을 갱신했는데 즉시 반영이 필요하면 콘솔의 **캐시 무효화** 버튼을 사용하세요. + +--- + +## 8. 서명키 회전 (`/admin/signing-keys`) + +OIDC/SAML 토큰 서명에 쓰이는 **RSA 서명키**를 생성·회전합니다. + +- 목록: `kid`, alg(RS256), 용도(use), **활성 여부(active)**, 인증서 보유 여부, 생성/회전/만료 시각. +- **회전(rotate)** 액션 한 번으로: + 1. 새 RSA 키 + 자체서명 인증서 생성(CN = issuer 호스트명, 없으면 `idp.local`). + 2. 기존 활성 키를 비활성화하고 새 키를 활성으로 **원자적(atomic)** 전환. + 3. partial unique index 로 **"동시에 활성 키는 항상 1개"** 불변식을 DB 레벨에서 보장(동시 회전 충돌 시 409 `rotate_conflict`). + 4. 로컬 캐시/JWKS 캐시 무효화(다른 isolate 는 캐시 TTL 만료로 수렴). +- 새 키의 private JWK 는 **`IDP_SIGNING_KEY_SECRET`** 로 래핑(AES-256-GCM)되어 저장됩니다. 따라서 이 시크릿이 없으면 회전이 503 으로 실패합니다. +- 공개키는 `/oidc/jwks` 로 노출됩니다. + +> **중요(혼동 주의)**: 이 화면의 "서명키 rotate" 는 **현재 `IDP_SIGNING_KEY_SECRET` 을 그대로 사용해 새 RSA 서명키를 만드는 것**입니다. 마스터 시크릿(`IDP_SIGNING_KEY_SECRET`) 자체의 회전과는 별개입니다. 마스터 시크릿 회전(무중단 절차, `IDP_SIGNING_KEY_SECRET_PREVIOUS` 병기, 재암호화 배치)은 **[docs/SECRET_ROTATION.md](./SECRET_ROTATION.md)** 를 따르세요. + +--- + +## 9. 감사 로그 조회 (`/admin/audit`) + +- 컬럼: 시각, **kind**(이벤트 종류), **outcome**(success/failure), IP, 사용자 이메일(연결된 경우), 상세(JSON). +- 필터: + - **kind**: 실제 존재하는 kind 목록에서 선택. + - **outcome**: `success` / `failure`. +- 페이징: 최신순 **50건**씩, 커서(마지막 행의 생성시각 기준) 기반 "더 보기". +- 감사 이벤트 행에는 무결성 MAC(`hash`)이 포함됩니다(위변조 탐지용, `IDP_SIGNING_KEY_SECRET` 기반). + +주요 kind 예: `login`, `user_created` / `user_invited` / `user_deleted`, `user_status_changed` / `user_role_changed`, `password_reset`, `oidc_client_*`, `saml_sp_*`, `service_role_*`, `signing_key_rotated`, `ldap_provider_*`, `user_deletion_requested` / `user_deletion_cancelled`. + +--- + +## 10. 사용자 운영 흐름 (`/admin/users`) + +목록은 최신순 50건 페이징 + 검색(이메일/아이디/표시이름 부분일치, 대소문자 무시). 유효한 미사용 초대 토큰을 가진 계정에는 **"초대중"** 배지가 붙습니다. + +### 관리자 작업 + +| 작업 | 동작 | +| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| **생성(create)** | 이메일+비밀번호(8자 이상) 즉시 계정 생성. 이메일/아이디 중복 시 409. | +| **초대(invite)** | 비밀번호 없이 계정 선생성(status=active, 이메일 미인증) 후 **초대 메일** 발송. 수락 시 최초 비밀번호 설정. | +| **상태 변경(updateStatus)** | active/disabled/locked. 비활성/잠금 전환 시 **기존 세션 즉시 파기** + 보안 알림 메일. 자기 자신 비활성화 및 **마지막 관리자 보호** 차단. | +| **역할 변경(updateRole)** | admin ↔ user. **자기 역할 변경 불가**, admin→user 강등 시 마지막 관리자 보호. 변경 시 세션 파기. | +| **비밀번호 초기화(resetPassword)** | 관리자가 새 비밀번호(8자 이상) 설정. 대상 사용자 **전 세션 파기** + 알림 메일. | +| **삭제(delete)** | 계정 하드 삭제. 자기 삭제 불가, 마지막 관리자 보호. | + +- 개별 사용자 상세(`/admin/users/[id]`)에서 프로필/조직 소속/서비스 권한/강제 로그아웃까지 관리합니다(2·4장 참조). + +### 이메일 인증 (self-service) + +- 가입/재발송 시 인증 토큰 발급 + 메일 발송(`/verify-email?token=...`). 토큰 유효기간 **24시간**. +- `IDP_ISSUER_URL` 미설정이면 host header injection 방지를 위해 메일 발송을 스킵합니다. + +### 초대 수락 (self-service) + +- 초대 링크(`/accept-invite?token=...`) 유효기간 **72시간**. 수락 시 사용자가 최초 비밀번호를 설정하며 토큰이 소비(used)됩니다. + +### 계정 삭제 유예 (self-service, `/account/danger-zone`) + +- 사용자 본인이 탈퇴를 요청하면: + 1. **step-up 재인증**(비밀번호 또는 TOTP) 필수 — 세션 탈취자에 의한 삭제 방지. + 2. **마지막 관리자 자기삭제 차단**. + 3. 계정을 **`status=deletion_pending` + `deletionScheduledAt`(now+30일)** 로 소프트 삭제. 전 세션·refresh token 즉시 폐기 후 로그아웃. 접수 알림 메일 발송. +- **복구(유예 내)**: 유예 30일 안에 다시 로그인하면 복구 확인 프롬프트가 뜨고, 비밀번호 재입력으로 확정하면 계정이 `active` 로 환원됩니다(`user_deletion_cancelled`). +- **유예 경과**: `deletionScheduledAt` 이 지난 계정은 로그인 거부되고, **GC 가 하드 삭제**합니다(감사 로그·복구 불가). + +--- + +### 참고 문서 + +- 마스터 시크릿 회전 절차: [docs/SECRET_ROTATION.md](./SECRET_ROTATION.md) diff --git a/src/routes/(auth)/mfa/+page.server.ts b/src/routes/(auth)/mfa/+page.server.ts index 35da423..74704f8 100644 --- a/src/routes/(auth)/mfa/+page.server.ts +++ b/src/routes/(auth)/mfa/+page.server.ts @@ -13,6 +13,10 @@ import { getRuntimeConfig } from "$lib/server/auth/runtime"; import { credentials, users } from "$lib/server/db/schema"; import { resolveSkinHtml, replacePlaceholders, escapeHtml } from "$lib/server/skin/resolver"; import { translate } from "$lib/i18n/server"; +import { dispatchSecurityAlert } from "$lib/server/security-notify"; + +// 백업 코드 저잔량 경고 임계값(이하이면 경고 알림). account/mfa 의 backupCodesRemaining 표시와 정합. +const BACKUP_CODES_LOW_THRESHOLD = 2; export const load: PageServerLoad = async ({ locals, cookies, platform, url }) => { const mfaToken = cookies.get(MFA_PENDING_COOKIE); @@ -210,6 +214,22 @@ export const actions: Actions = { return fail(400, { error: msg, skinHtml: await resolveMfaSkinForAction(event, msg) }); } + // 백업 코드로 통과한 경우: 소진 처리 후 남은 미사용 코드 수를 계산해 + // 저잔량(≤임계값) 경고 / 소진(0) 알림을 보안 메일로 발송한다(fire-and-forget). + // TOTP 로 통과한 경우엔 백업 코드가 소비되지 않으므로 검사하지 않는다(오탐 방지). + if (useBackup) { + const remainingRows = await db + .select({ id: credentials.id }) + .from(credentials) + .where(and(eq(credentials.userId, user.id), eq(credentials.type, BACKUP_CODE_CREDENTIAL_TYPE), isNull(credentials.usedAt))); + const remaining = remainingRows.length; + if (remaining === 0) { + dispatchSecurityAlert({ to: user.email, locale: user.locale, kind: "backup_codes_depleted", platform: event.platform }); + } else if (remaining <= BACKUP_CODES_LOW_THRESHOLD) { + dispatchSecurityAlert({ to: user.email, locale: user.locale, kind: "backup_codes_low", platform: event.platform }); + } + } + // MFA 통과 — 세션 생성 event.cookies.delete(MFA_PENDING_COOKIE, { path: "/" }); diff --git a/src/routes/admin/skins/+page.svelte b/src/routes/admin/skins/+page.svelte index c56aedb..82bef77 100644 --- a/src/routes/admin/skins/+page.svelte +++ b/src/routes/admin/skins/+page.svelte @@ -273,5 +273,8 @@ function clientLabel(clientType: string, clientRefId: string): string {

{{IDP_FORM_ACTION}} — {t("skins.placeholder_form_action")}

{{IDP_REDIRECT_TO}} — {t("skins.placeholder_redirect_to")}

{{IDP_SKIN_HINT}} — {t("skins.placeholder_skin_hint")}

+

{{IDP_REGISTERED}} — {t("skins.placeholder_registered")}

+

{{IDP_PASSWORD_RESET}} — {t("skins.placeholder_password_reset")}

+

{{IDP_FLASH_MSG}} — {t("skins.placeholder_flash_msg")}

diff --git a/src/routes/admin/skins/guide/+page.svelte b/src/routes/admin/skins/guide/+page.svelte index dece0e6..46dcf4c 100644 --- a/src/routes/admin/skins/guide/+page.svelte +++ b/src/routes/admin/skins/guide/+page.svelte @@ -51,7 +51,7 @@ import { t } from "$lib/i18n.svelte"; - {#each [{ key: "IDP_FORM_ACTION", desc: t("skins.placeholder_form_action") }, { key: "IDP_REDIRECT_TO", desc: t("skins.placeholder_redirect_to") }, { key: "IDP_SKIN_HINT", desc: t("skins.placeholder_skin_hint") }] as row (row.key)} + {#each [{ key: "IDP_FORM_ACTION", desc: t("skins.placeholder_form_action") }, { key: "IDP_REDIRECT_TO", desc: t("skins.placeholder_redirect_to") }, { key: "IDP_SKIN_HINT", desc: t("skins.placeholder_skin_hint") }, { key: "IDP_REGISTERED", desc: t("skins.placeholder_registered") }, { key: "IDP_PASSWORD_RESET", desc: t("skins.placeholder_password_reset") }, { key: "IDP_FLASH_MSG", desc: t("skins.placeholder_flash_msg") }] as row (row.key)} {{{row.key}}} From 5f8dbe606d29e7f5b0e120e15623da2b2c431464 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Mon, 6 Jul 2026 17:45:47 +0900 Subject: [PATCH 25/26] =?UTF-8?q?docs:=20=ED=9B=84=EC=86=8D=203=EC=B0=A8?= =?UTF-8?q?=20=EB=A7=88=EA=B0=90(round3)=20=EA=B3=84=ED=9A=8D=C2=B7?= =?UTF-8?q?=ED=88=AC=EB=91=90=20=EB=AC=B8=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - followup-round3/PLAN.md·TODO.md: P12~P17 페이즈 설계와 페이즈별 실행 투두·검증 기록 Co-Authored-By: Claude Fable 5 --- docs/plans/followup-round3/PLAN.md | 123 +++++++++++++++++ docs/plans/followup-round3/TODO.md | 212 +++++++++++++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 docs/plans/followup-round3/PLAN.md create mode 100644 docs/plans/followup-round3/TODO.md diff --git a/docs/plans/followup-round3/PLAN.md b/docs/plans/followup-round3/PLAN.md new file mode 100644 index 0000000..a4a14ed --- /dev/null +++ b/docs/plans/followup-round3/PLAN.md @@ -0,0 +1,123 @@ +# 후속 3차 개선 기획서 — KeyStone (마감 라운드) + +> 요청: 남은 항목 전부 진행 (C 기능·UX 마감 + A rate-limit 추상화 + B 테스트 심화 + 새 전수 재조사). +> 조사: 2026-07-06, 4개 병렬 Explore(전수 재조사=Opus, A/B/C=Sonnet). 코드 근거 확인. +> 규칙: 스키마 변경은 `db:generate:all`까지만(적용은 사용자). 커밋은 검증 통과 + 승인 후. + +--- + +## 배경 / 목표 + +두 라운드로 핵심 결함·기능 공백은 해소됨. 이번은 **마감 라운드**: 전수 재조사로 드러난 신규 갭 + 미룬 A/B/C를 처리한다. 조사 종합 결과 6개 페이즈: + +- **P12 잔여 버그·보안·운영 마감** — 전수 재조사 발견(저비용 고가치). +- **P13 메일 i18n + 세션 철회 알림 + 프로필 이메일 변경** — 기능 공백. +- **P14 UX a11y/loading + 스킨 복구 패널** — 접근성·사용성. +- **P15 rate-limit 저장소 추상화(A)** — 인터페이스 + Node in-memory. +- **P16 테스트 심화(B)** — SAML SSO·로그인+MFA·LDAP 통합 테스트. +- **P17 문서·잔여 마감** — admin 매뉴얼, organization 세분화, 관측성. + +--- + +## 범위 (In / Out) + +### Phase 12 — 잔여 버그·보안·운영 마감 (스키마 소폭) + +- **In**: + - **B1 invite_tokens GC 누락**(Medium): `gc.ts`가 users 하드삭제는 넣으면서 invite_tokens(만료/소진) purge를 누락 — 무한 성장. 다른 토큰 테이블과 동일 시맨틱으로 추가. + - **B2 organization scope id_token 불일치**(표준 위배): `oidc/token/+server.ts`가 `scopes.has("organization")`을 안 봐서 organization만 요청 시 id_token에 조직정보 누락(userinfo만 실림). userinfo와 동일 로직 공유로 수정. + - **B3 accept-invite status 검증 + 초대계정 상태**(Low보안): accept-invite `lookupToken`에 `status="active"` 필터 추가(disabled/deletion_pending 계정에 credential 심기 방지). 초대 미수락 계정이 즉시 `active`라 admin 카운트 오염 — credential 존재 기준으로 admin 판정하거나 상태 구분. + - **B4 헬스체크 readiness**(Low운영): `api/health`가 DB 다운에도 200 → DB unavailable 시 503. + - **B5 users 하드삭제 인덱스+배치**(Low): `deletionScheduledAt`/`status` 부분 인덱스 + GC 배치 LIMIT(대량 cascade 락 방지). `db:generate:all`. + - **B6 잔여 하드코딩 한국어**(Low): `account/profile/+page.server.ts:52`(생년월일), `accept-invite:77`(credential label) translate 전환. +- **Out**: DCR/PAR/JAR/Device Flow(로드맵 — 정책 판단, 열린 질문 ⑤), 계정 병합. + +### Phase 13 — 메일 i18n + 세션 철회 알림 + 프로필 이메일 변경 + +- **In**: + - **F1 메일 전량 locale 인지**: 재설정/인증/초대 메일이 수신자 locale 무시(ko 고정) — `email.ts` 템플릿을 locale 인자 받도록(보안 알림 메일 패턴 확장). `baseHtml` 푸터·제목 i18n. + - **F2 세션 철회 보안 알림**: `account/sessions` revoke/revokeOthers에 `dispatchSecurityAlert` 추가(`session_revoked`/`sessions_revoked_all` kind + i18n). 본인 직접 철회에도 보낼지는 열린 질문 ④(기본: 보냄 — 탈취 방어). + - **F3 프로필 이메일 변경**(중간~높음): `users.pendingEmail`+`pendingEmailRequestedAt` 컬럼, `account/profile` changeEmail 액션(현 비밀번호 재인증 → pendingEmail 저장 → 새 주소 인증 메일), confirm 라우트(토큰 검증 → email 교체). 기존 이메일에 "변경 시도" 알림(탈취 방어). `email_change_tokens` 또는 email_verification_tokens 파라미터화. `db:generate:all`. +- **Out**: 이메일 변경 시 기존 세션 유지 정책(변경만, 세션 무효화는 안 함 — 표준). + +### Phase 14 — UX a11y/loading + 스킨 복구 패널 + +- **In**: + - **U1 a11y 에러 배너**: 8개 auth 라우트 에러 배너에 `role="alert"`/`aria-live="assertive"` 추가. 실패 후 포커스 이동. + - **U2 loading 상태**: 메인 제출 폼(login/signup/mfa/reset/find-\*/accept-invite)에 `use:enhance` + 제출 중 버튼 disabled/스피너. 공용 패턴/컴포넌트로 추출. + - **U3 스킨 로그인 복구 패널**(옵션 B, 저비용): `login/+page.svelte`의 스킨 조건을 `{#if skinHtmlEffective && !form?.recovery}`로 바꿔 복구 케이스는 기본 UI 강제 노출(스킨 작성자 개입 불필요). 또는 recovery 반환에 skinHtml 채우기. +- **Out**: 전면 스킨 슬롯 시스템(옵션 A — 등록 스킨 소급 수정 부담 큼). + +### Phase 15 — rate-limit 저장소 추상화 (A) + +- **In**: + - **A1 인터페이스**: `RateLimitStore { increment(key,windowMs), peek(key,windowMs) }` 추출. `checkRateLimit(store, key, opts)`로 슬라이딩 윈도우 알고리즘 유지·저장소만 위임. `login`의 중복 `accountLockStatus` 구현을 `store.peek()`로 대체. + - **A2 Node in-memory**: `Map` 기반 store(기존 globalThis 전역 패턴). 단일 인스턴스 가정 한계 문서화(다중 인스턴스는 Redis 필요 — 이번 미도입). + - **A3 Workers 저장소**: **DO는 adapter-cloudflare 제약(fetch 단일 export)으로 wrapper 엔트리 필요 = 과침습**(GC scheduled 포기와 동일). 열린 질문 ①에 따라 (a) Workers는 현행 DB 저장소 유지(인터페이스만 추상화, 나중에 DO 삽입 가능), (b) wrapper 엔트리 도입해 DO. **기본안: (a)** — 추상화로 정리 효과(DB 4방언 분기·중복 제거)만 취하고 DO는 별도 결정. + - 21곳 호출부 시그니처 교체(기계적), `locals.rateLimitStore` 요청당 1회 해석(db.ts 패턴). +- **Out**: Redis(다중 인스턴스 시에만), KV(원자 increment 없어 보안 rate-limit 부적합). + +### Phase 16 — 테스트 심화 (B, Playwright 제외) + +- **In**: + - **T1 하네스 확장**: `seedSamlSp`, `seedIdentityProvider`, cookies 체이닝 헬퍼, SAML 서명 fixture 헬퍼(기존 유닛 테스트 `makeKeyCert`/`signAuthnRequest` 승격). + - **T2 SAML SSO 통합**: SP-initiated POST 바인딩(authorize→Response 검증, 권한 게이트, replay 가드). saml 커버리지 15%→상승. + - **T3 로그인+MFA 체이닝 통합**: 같은 cookies 인스턴스 재사용으로 idp_session/idp_mfa_pending 왕복. auth 34%→상승. + - **T4 LDAP 로그인 통합**: `seedIdentityProvider`로 login LDAP 분기. ldap 0%→상승. +- **Out**: admin CRUD 전체 통합(라우트 다수 — 시간 크나 선택적, 열린 질문 없이 여력 되면), SAML Redirect 바인딩 서명(반나절 추가), Playwright e2e(ROI 최하 — 통합으로 대부분 커버). + +### Phase 17 — 문서·잔여 마감 + +- **In**: + - **D1 admin 운영 매뉴얼**: `docs/ADMIN_GUIDE.md`(조직/클라이언트/스킨/서명키/감사로그). skins/guide placeholder 6개 실제값 문서화(가이드 페이지도 갱신). + - **D2 organization 노출 세분화 UI**(중간, 열린 질문 ③): oidc-clients 상세에 organization 클레임 토글(department/team/position 개별). `oidcClients.organizationClaimConfig` JSON 컬럼 + userinfo/token 참조. `db:generate:all`. + - **D3 관측성 훅**(Low): 헬스체크 개선(B4와 통합), GC 결과 구조화 로깅. 구조화 로거·request-id·메트릭 export는 기반만(전면 도입은 별도). + - **D4 백업코드 저잔량 경고**(Low): 로그인 시 backup_code 소비 후 잔량 경고, 소진 시 알림 메일. +- **Out**: Sentry/OTel 전면 배선, 감사뷰어 필터 대폭 확장(날짜/actor/CSV — 별도), scope 자유텍스트→체크박스 전면 개편. + +--- + +## 코드베이스 사실관계 (조사 근거) + +- **invite_tokens GC 누락**: `gc.ts:26,157-188` — users 하드삭제는 추가됐으나 invite_tokens purge 없음(다른 토큰과 동일 시맨틱). +- **organization 버그**: `oidc/token/+server.ts:148-153`이 `groups`만 체크, userinfo(`userinfo:120-154`)는 department/team/position 매핑 → organization scope만 요청 시 id_token 누락. +- **accept-invite**: `accept-invite:15-27` status 필터 없음. 초대 계정 `admin/users:227`이 즉시 `status:"active"`. +- **헬스체크**: `api/health:11-21` DB 상태 무관 200. +- **rate-limit**: `ratelimit/index.ts`(86줄) 2버킷 슬라이딩 윈도우, DB 4방언 분기. 호출부 21곳 `rl.allowed`만 사용(`remaining` 미참조). `login`이 `accountLockStatus`로 peek 중복 구현(20-46). DO는 adapter 제약(`_worker.js` fetch 단일 export)으로 wrapper 엔트리 필요. +- **테스트 하네스**: `harness.ts`가 makeEvent/makeCookies/seed 유틸 제공. cookies 인스턴스 재사용으로 라우트 체이닝 가능(신규 패턴). SAML fixture는 `test/unit/saml-verify-xml-signature.test.ts`의 makeKeyCert/signAuthnRequest 재사용. 커버리지: saml 15%·ldap 0%·org 0%·access 0%·admin/user-actions 0%. adapter-node는 이미 설정됨(svelte.config.js BUILD_TARGET 분기). Playwright 미설치. +- **a11y**: 8개 auth 폼 에러 배너 role/aria 전무, logout만 use:enhance. +- **프로필 이메일 변경**: `account/profile:35-71` email 필드 없음, pendingEmail 컬럼 없음. +- **메일 i18n**: 보안 알림만 locale 인자, reset/verify/invite 메일은 ko 고정. +- **스킨 복구**: `login/+page.server.ts:271-274` recovery 반환에 skinHtml 없음 → 스킨 로그인에서 복구 패널 미표시(무한 로그인 폼). + +--- + +## 접근법 / 리스크 / 순서 + +| Phase | 접근 | 리스크 & 완화 | +| ------------- | ------------------------------------------------------------- | -------------------------------------------------------------- | +| 12 마감 | 기존 패턴 소폭 수정, 저비용 | organization 버그는 표준 정합 회귀 주의 → 통합 테스트로 커버 | +| 13 기능 | email.ts locale화 + pendingEmail 플로우(email 인증 패턴 복제) | 이메일 변경은 탈취 벡터 → 재인증+기존주소 알림 필수 | +| 14 UX | role=alert/use:enhance 일관 적용(공용화) | 스킨 복구는 옵션 B(코드만)로 스킨 작성자 무개입 | +| 15 rate-limit | 인터페이스 추상화 + Node in-memory, Workers는 기본 DB 유지 | DO는 과침습이라 열린 질문 ①. 추상화만으로 4방언 분기 제거 효과 | +| 16 테스트 | 하네스 확장(기존 fixture 재사용) | Playwright 제외(ROI). 커버리지 0% 구간 집중 | +| 17 문서 | 문서 + organization UI(스키마 소폭) + 관측성 기반 | organization UI는 열린 질문 ③(버그만 vs UI까지) | + +**순서**: 12(마감·기반) → 13/14 병렬(기능·UX, 파일 대체로 분리) → 15(rate-limit, 21곳 광범위) → 16(테스트 — 12~15 신규 로직 포함) → 17(문서·마감). 스키마 건드리는 12(B5)·13(F3)·17(D2)는 마이그레이션 순서 조율(직렬). + +--- + +## 열린 질문 (승인 시 확정) + +1. **rate-limit Workers 저장소(P15)** — (a) 인터페이스만 추상화하고 Workers는 현행 DB 유지(Node만 in-memory 개선, 권장 — 저리스크), (b) wrapper 엔트리 도입해 Durable Object(성능 최적이나 과침습·빌드구조 변경). 어느 쪽? +2. **프로필 이메일 변경(P13 F3)** — 포함할지(중간~높음, 스키마+confirm 라우트). 표준 IdP 기능이라 권장하나 규모 있음. +3. **organization 노출 세분화 UI(P17 D2)** — id_token 불일치 버그 수정(P12 B2)만 할지, 클라이언트별 세분화 토글 UI(스키마 컬럼)까지 할지. +4. **세션 철회 알림(P13 F2)** — 본인이 직접 자기 세션 철회할 때도 알림 메일 보낼지(탈취 방어 vs 정상 로그아웃 스팸). 기본: 보냄. +5. **DCR/PAR/Device Flow(P12 Out)** — 표준 확장. 이번 로드맵 제외가 기본. 원하면 별도 트랙. + +--- + +## 산출물 + +- `docs/plans/followup-round3/PLAN.md` (본 문서) → 승인 후 `TODO.md` +- 스키마 변경분은 `db:generate:all`까지만 — 적용은 사용자. diff --git a/docs/plans/followup-round3/TODO.md b/docs/plans/followup-round3/TODO.md new file mode 100644 index 0000000..4c1c45b --- /dev/null +++ b/docs/plans/followup-round3/TODO.md @@ -0,0 +1,212 @@ +# 실행 투두 — 후속 3차 마감 (6 페이즈) + +> 승인(2026-07-06, 게이트①): 기획 승인 · P15 rate-limit = **인터페이스만 추상화**(Workers DB 유지, Node in-memory, DO 미도입) · 기능 = **프로필 이메일 변경 + organization 세분화 UI + 백업코드 경고 전부 포함** · 세션 철회 알림 = **본인 직접 철회도 발송**. +> 규칙: 스키마 변경은 `db:generate:all`까지만(적용 금지). 커밋은 전체 검증 통과 + 커밋 승인 후, 푸시 금지. 쓰기=Opus 위임, 검증=작성자와 분리된 독립 Opus. 스텁/TODO/skip은 블로커. +> 순서: P12 → P13/P14 병렬 → P15 → P16(신규 로직 통합) → P17. 스키마 건드리는 P12(B5)·P13(F3)·P17(D2)는 마이그레이션 순서 직렬. + +--- + +## Phase 12 — 잔여 버그·보안·운영 마감 + +**목적**: 전수 재조사 발견 항목(저비용 고가치) 해소. + +### [x] 12-1. B1 invite_tokens GC 누락 (Medium) + +- 파일: `src/lib/server/db/gc.ts` +- 작업: invite_tokens purge 추가(다른 토큰 테이블과 동일 시맨틱 — `lt(expiresAt, now)` 또는 `or(lt(expiresAt,now), isNotNull(usedAt))`). 기존 테이블별 에러 격리 패턴 준수. `test/unit/gc.test.ts`에 조건 테스트 추가. +- 수용 기준: invite_tokens가 GC 대상에 포함, 미만료·미소진 보존. gc 테스트 통과. + +### [x] 12-2. B2 organization scope id_token 불일치 (표준 위배) + +- 파일: `src/routes/oidc/token/+server.ts`, 참고 `oidc/userinfo/+server.ts:120-154` +- 작업: token의 id_token 클레임 생성에 `scopes.has("organization")` 분기 추가(userinfo와 동일하게 department/team/position/job_title 매핑). 공통 로직은 `oidc/claims.ts`로 추출해 token/userinfo 공유(중복 방지). `groups`와 `organization`의 각 노출 범위를 정확히 재현. +- 수용 기준: organization scope 요청 시 id_token·userinfo가 동일 조직 클레임. groups scope 동작 무회귀. 통합 테스트(P16)로 커버. + +### [x] 12-3. B3 accept-invite status 검증 + 초대계정 상태 (Low보안) + +- 파일: `src/routes/(auth)/accept-invite/+page.server.ts`, `src/routes/admin/users/+page.server.ts` +- 작업: (a) `lookupToken` 조인에 `eq(users.status, "active")` 추가, 소진 직전 status 재확인(disabled/deletion_pending 계정에 credential 심기 방지). (b) 초대 미수락 계정의 admin 카운트 오염 — `assertNotLastAdmin`/admin 집계가 credential 존재(=실사용 가능)를 기준으로 판정하도록 보강. 초대 계정 상태 표현은 기존 배지 로직(invite_token 보유)과 정합 유지. +- 수용 기준: 비활성/삭제예정 계정은 초대 수락 불가. 미수락 초대 admin이 마지막 admin 삭제 차단을 잘못 완화하지 않음. + +### [x] 12-4. B4 헬스체크 readiness (Low운영) + +- 파일: `src/routes/api/health/+server.ts` +- 작업: DB 연결 확인(가벼운 SELECT 1) 후 실패 시 503, 성공 시 200. liveness/readiness 구분이 유용하면 쿼리파라미터로. 기존 응답 형식 최대한 보존. +- 수용 기준: DB unavailable 시 503. 정상 시 200. 과도한 부하 없음(경량 쿼리). + +### [x] 12-5. B5 users 하드삭제 인덱스+배치 (Low) + +- 파일: `src/lib/server/db/schema.{sqlite,pg,mysql}.ts`, `src/lib/server/db/gc.ts` +- 작업: users에 `deletionScheduledAt`(+status) 조회를 지원하는 인덱스 추가(가능하면 부분 인덱스 `WHERE status='deletion_pending'` — 방언별 표현, mysql 미지원 시 일반 인덱스). GC users 하드삭제에 배치 LIMIT(대량 cascade 락 방지). `db:generate:all`. +- 수용 기준: 3방언 parity(인덱스 예외 목록 갱신). GC가 배치 삭제. 미경과 계정 미삭제 유지. + +### [x] 12-6. B6 잔여 하드코딩 한국어 (Low) + +- 파일: `src/routes/account/profile/+page.server.ts:52`(생년월일 형식), `src/routes/(auth)/accept-invite/+page.server.ts:77`(credential label) +- 작업: `translate(locals.locale, ...)` 전환, ko/en 키 추가. (email.ts 메일 i18n은 P13 F1에서.) +- 수용 기준: 대상 2곳 한국어 리터럴 제거, ko/en 대칭. + +### [x] 12-검증 (독립 Opus) — 통과(2026-07-06, 로직 VERIFIED): organization 버그 표준정합·groups 무회귀, admin카운트·GC·헬스체크·인덱스 정확(prettier 정리). V2/V3/V6 전용 테스트는 P16에 반영 + +- organization 버그 수정의 표준 정합(id_token=userinfo), GC 조건 보수성, accept-invite status 가드, 헬스체크 503, 인덱스 parity. 게이트 4종. + +--- + +## Phase 13 — 메일 i18n + 세션 철회 알림 + 프로필 이메일 변경 + +**목적**: 메일 locale 인지, 세션 철회 알림, 이메일 변경 플로우. +**파일 경계**: F1(email.ts)·F2(sessions)·F3(profile+email.ts) — email.ts 공유이므로 F1→F2/F3 순서 또는 단일 에이전트. + +### [x] 13-1. F1 메일 전량 locale 인지 + +- 파일: `src/lib/server/email.ts`, 발송 호출부(reset-password/verify-email/accept-invite/invite) +- 작업: `sendPasswordResetEmail`/`sendEmailVerificationEmail`/`sendInviteEmail`·`baseHtml`을 수신자 locale 인자 받도록(보안 알림 메일 패턴 확장). 제목·본문·푸터·`lang` 속성 i18n. 호출부에서 대상 사용자 locale 전달(없으면 요청 locale 또는 ko 기본). i18n 키 ko/en. +- 수용 기준: 각 메일이 수신자 locale로 렌더. escape/safe-URL 유지. 발송 best-effort 유지. + +### [x] 13-2. F2 세션 철회 보안 알림 + +- 파일: `src/routes/account/sessions/+page.server.ts`, `src/lib/server/security-notify.ts`(kind 추가), i18n +- 작업: `SecurityEventKind`에 `session_revoked`/`sessions_revoked_all` 추가 + `security_alert.*` ko/en 3줄씩. sessions revoke/revokeOthers 액션에 `dispatchSecurityAlert`(best-effort) 추가 — **본인 직접 철회도 발송**(승인됨). 현재 세션 철회(로그아웃) 케이스에도 일관. +- 수용 기준: 세션 철회 시 알림 메일 경로 존재. best-effort 격리. i18n 대칭. + +### [x] 13-3. F3 프로필 이메일 변경 (중간~높음) + +- 파일: `src/lib/server/db/schema.{sqlite,pg,mysql}.ts`(`pendingEmail`+`pendingEmailRequestedAt`), `src/routes/account/profile/+page.server.ts`(changeEmail 액션)·`+page.svelte`, confirm 라우트 `src/routes/account/confirm-email-change/`(신규), 토큰(email_verification_tokens 파라미터화 또는 신규 `email_change_tokens`), `security-notify.ts`(email_change kind), i18n +- 작업: changeEmail 액션 — **현 비밀번호 재인증** → 중복 이메일 체크 → `pendingEmail` 저장 + 새 주소로 인증 토큰 메일 → confirm 라우트에서 토큰 검증 후 `email` 교체·`pendingEmail` 클리어·`emailVerifiedAt` 갱신(원자적 `runAtomic`). **기존 이메일에 "변경 시도" 알림**(탈취 방어). profile UI에 이메일 input + 현 비밀번호 + "확인 대기" 배너. rate-limit. `db:generate:all`. +- 수용 기준: 이메일 변경→새 주소 인증→교체 동작. 재인증 없이 변경 불가. 기존 주소 알림. 토큰 1회용·만료. 중복 이메일 차단. 스키마 parity. + +### [x] 13-검증 (독립 Opus) — 통과(APPROVE): 메일 locale, 세션 알림, 이메일변경(재인증·targetEmail 바인딩·원자성·기존주소 알림). profile UI 별도 마무리. sessions 하드코딩 한국어 2건은 P17로 + +- 메일 locale 렌더, 세션 알림 격리, 이메일 변경의 재인증·토큰 보안·기존주소 알림·원자성·중복 차단. 마이그레이션 생성만. 게이트. + +--- + +## Phase 14 — UX a11y/loading + 스킨 복구 패널 + +**목적**: 접근성·제출 피드백·스킨 복구. + +### [x] 14-1. U1 a11y 에러 배너 + 포커스 + +- 파일: 8개 auth `.svelte`(login/signup/mfa/reset-password/find-id/find-password/accept-invite/verify-email), 가능하면 공용 컴포넌트 `src/lib/components/FormError.svelte`(신규) +- 작업: 에러 배너에 `role="alert"`/`aria-live="assertive"`. 실패 후 배너/실패 필드로 포커스 이동(`tabindex=-1`+focus 또는 autofocus 로직). 공용 컴포넌트로 추출해 일관 적용. +- 수용 기준: 8개 폼 에러가 스크린리더 announce. 포커스 이동. 시각 동작 무회귀. svelte-autofixer 통과. + +### [x] 14-2. U2 loading 상태 + +- 파일: 위 8개 폼(패스키 별도 흐름 제외), 공용 enhance 패턴 +- 작업: 메인 제출 폼에 `use:enhance` + 제출 중 버튼 disabled + 스피너/텍스트. SSR 폴백 유지(JS 없어도 동작). 공용 패턴/컴포넌트로. +- 수용 기준: 제출 중 중복 클릭 방지·로딩 표시. JS 비활성에도 폼 동작(progressive enhancement). 기존 리다이렉트/에러 흐름 무회귀. + +### [x] 14-3. U3 스킨 로그인 복구 패널 (옵션 B) + +- 파일: `src/routes/(auth)/login/+page.svelte`, 필요시 `+page.server.ts` +- 작업: 스킨 렌더 조건을 `{#if skinHtmlEffective && !form?.recovery}`로 바꿔 복구(soft-delete) 케이스는 기본 UI 강제 노출(스킨 작성자 개입 불필요). 또는 recovery 반환에 skinHtml 채우고 기본 패널 우선. 스킨 로그인에서도 복구 프롬프트·`recover=1` 접근 가능. +- 수용 기준: 스킨 로그인에서 삭제예정 계정이 복구 패널 도달. 일반 스킨 로그인 무회귀. + +### [x] 14-검증 (독립 Opus) — 통과(APPROVE): FormError role=alert+포커스, use:enhance progressive enhancement(JS off 동작), 스킨 복구 도달. 803키 대칭 + +- role/aria 적용, 포커스, progressive enhancement(JS off 동작), 스킨 복구 도달. 게이트. + +--- + +## Phase 15 — rate-limit 저장소 추상화 (인터페이스만, Node in-memory) + +**목적**: 저장소 추상화로 DB 4방언 분기·login 중복 제거. Workers는 현행 DB 유지, Node는 in-memory. + +### [x] 15-1. A1 RateLimitStore 인터페이스 + +- 파일: `src/lib/server/ratelimit/store.ts`(신규 인터페이스+구현), `src/lib/server/ratelimit/index.ts` +- 작업: `RateLimitStore { increment(key,windowMs): {current,prev}; peek(key,windowMs): {current,prev} }` 정의. `checkRateLimit(store, key, opts)`로 슬라이딩 윈도우 수식 유지·저장소 위임. **DbRateLimitStore**(현 DB 로직 이관 — 4방언 분기 캡슐화)와 **MemoryRateLimitStore**(Map) 구현. `locals.rateLimitStore`를 hooks에서 요청당 1회 해석(Workers=DB, Node=memory; `db/index.ts` isWorkers 판별 재사용). +- 수용 기준: 알고리즘 동일(기존 ratelimit 테스트 통과). peek/increment 분리. Workers=DB·Node=memory 분기. + +### [x] 15-2. A2 호출부 이관 + login 중복 제거 + +- 파일: rate-limit 호출부 21곳, `src/routes/(auth)/login/+page.server.ts`(accountLockStatus) +- 작업: `checkRateLimit(db,...)` → `checkRateLimit(locals.rateLimitStore,...)` 기계적 교체. login의 중복 `accountLockStatus` 구현을 `store.peek()`로 대체. 기존 `rate_limits` 테이블·스키마·`purgeExpiredRateLimits`/gc 호출은 Workers(DB store)가 계속 쓰므로 **유지**(Node memory는 자체 evict). +- 수용 기준: 21곳 정상 동작(429 분기 무회귀). login 계정 잠금 동작 동일. Node/Workers 양쪽 rate-limit 동작. + +### [x] 15-3. A3 Node in-memory 한계 문서화 + +- 파일: `.env.example` 또는 README 관련 섹션 +- 작업: Node in-memory rate-limit이 단일 인스턴스 가정임을 명시(다중 인스턴스 확장 시 한도 완화 → Redis store 필요, 이번 미도입). 인터페이스가 Redis/DO store 추가에 열려있음 안내. +- 수용 기준: 한계·확장 경로 문서화. + +### [x] 15-검증 (독립 Opus) — 통과(APPROVE): 알고리즘 byte-identical, peek=accountLockStatus 정합, 24곳 무회귀, Workers/Node 빌드 그린. single-now 스레딩 미세개선 별도 처리 + +- 알고리즘 동일성(peek/increment가 기존 upsert+select와 같은 결과), 21곳 무회귀, login 잠금 정합, Node memory evict·격리. 게이트 + rate-limit 통합/유닛 테스트. + +--- + +## Phase 16 — 테스트 심화 (하네스 확장, Playwright 제외) + +**목적**: 커버리지 0% 구간(saml/ldap) + auth 통합. P12~15 신규 로직 포함. + +### [x] 16-1. T1 하네스 확장 + +- 파일: `test/integration/harness.ts` +- 작업: `seedSamlSp`(samlSps 삽입), `seedIdentityProvider`(LDAP provider), cookies 체이닝 헬퍼(같은 makeCookies 재사용 공식화), SAML 서명 fixture(`test/unit/saml-verify-xml-signature.test.ts`의 makeKeyCert/signAuthnRequest 승격·공용화). +- 수용 기준: 헬퍼 추가, 기존 통합 테스트 무영향. + +### [x] 16-2. T2 SAML SSO 통합 테스트 + +- 파일: `test/integration/saml-sso.test.ts`(신규) +- 작업: SP-initiated POST 바인딩 — 로그인 상태에서 authorize→SAML Response 검증(서명·audience·ACS), 서비스 권한 게이트 실패, AuthnRequest replay 가드. 실 서명키·실 라우트. +- 수용 기준: 풀플로우 그린, saml 커버리지 상승. tautology 없음. + +### [x] 16-3. T3 로그인+MFA 체이닝 통합 + +- 파일: `test/integration/login-mfa.test.ts`(신규) +- 작업: 같은 cookies 인스턴스로 login(비번)→idp_mfa_pending→mfa(TOTP)→idp_session 발급 검증. 잘못된 코드 거부, rate-limit, 백업코드 경로(P17 D4와 정합). +- 수용 기준: 쿠키 왕복 재현, auth 커버리지 상승. + +### [x] 16-4. T4 LDAP 로그인 통합 + 신규 로직 통합 + +- 파일: `test/integration/ldap-login.test.ts`(신규), 기존 통합에 P12~15 신규 로직 추가 +- 작업: `seedIdentityProvider`로 login LDAP 분기(가능 범위 — 실 LDAP 서버 없이 되는 부분, mock LDAP client 필요 시 사유 보고). organization id_token 일치(B2), 이메일 변경(F3), rate-limit store(A) 통합 검증. +- 수용 기준: ldap 분기 커버(가능 범위), 신규 로직 통합 그린. 무리한 것은 제외·사유 보고. + +### [x] 16-검증 (독립 Opus) — 통과(11기준 VERIFIED): SAML SSO·login+MFA·LDAP·P12로직·세션알림 통합 전부 실 DB/실 라우트(mock-echo 없음), single-now 정합. 총 211테스트(prettier 정리) + +- 실 DB/실 라우트 구동(mock-echo 아님), fixture 정합, tautology·skip 없음. 게이트 + 전체 테스트 수. + +--- + +## Phase 17 — 문서·잔여 마감 + +**목적**: admin 매뉴얼, organization 세분화 UI, 백업코드 경고, 관측성 기반. + +### [x] 17-1. D1 admin 운영 매뉴얼 + +- 파일: `docs/ADMIN_GUIDE.md`(신규), `src/routes/admin/skins/guide/+page.svelte`(placeholder 갱신) +- 작업: 조직(dept/team/part/position) 관리, OIDC/SAML 클라이언트 등록, scope/role 설정, 스킨 등록 + placeholder 6종 전체(`IDP_FORM_ACTION`/`IDP_REDIRECT_TO`/`IDP_SKIN_HINT`/`IDP_REGISTERED`/`IDP_PASSWORD_RESET`/`IDP_FLASH_MSG`) 문서화, 서명키 회전, 감사로그 조회. skins/guide 페이지의 불완전한 placeholder 문서도 갱신. +- 수용 기준: 매뉴얼 존재, placeholder 실제값 정확. + +### [x] 17-2. D2 organization 노출 세분화 UI (중간) + +- 파일: `src/lib/server/db/schema.*.ts`(`oidcClients.organizationClaimConfig` JSON), `src/routes/admin/oidc-clients/[id]/+page.svelte`·server, `oidc/claims.ts`/token/userinfo(config 참조), i18n +- 작업: 클라이언트별 organization 클레임 토글(department/team/position/job_title 개별 노출). config 컬럼 추가, 매핑 로직이 config 참조(미설정 시 기존 전량 노출로 하위호환). oidc-clients 상세에 체크박스 그룹 + 설명. `db:generate:all`. +- 수용 기준: 토글대로 클레임 노출. 기존 클라이언트 하위호환(config null=전량). B2(id_token 일치)와 정합. parity. + +### [x] 17-3. D4 백업코드 저잔량 경고 + +- 파일: `src/routes/(auth)/mfa/+page.server.ts`, `account/mfa`, security-notify(kind), i18n +- 작업: 로그인 시 backup_code 소비 후 잔량 계산 → 저잔량(예: ≤2) 경고 표시, 소진 시 알림 메일(`dispatchSecurityAlert` kind 추가). account/mfa의 잔량 표시와 정합. +- 수용 기준: 저잔량 경고·소진 알림. 오탐 없음. + +### [x] 17-4. D3 관측성 기반 (Low) + +- 파일: `src/lib/server/db/gc.ts`(GC 결과 구조화 로깅), 필요시 로깅 헬퍼 +- 작업: GC 결과·주요 이벤트를 구조화 형태(JSON, tenant/requestId 가능 시)로 로깅하는 최소 기반. 전면 로거/메트릭/Sentry는 별도(범위 밖) — 훅 지점만 정리. +- 수용 기준: GC 결과 구조화 로그. 과설계 없음. + +### [x] 17-검증 (독립 Opus) — 통과(로직 VERIFIED): organization 하위호환·id_token=userinfo 정합, 백업코드 오탐 방지, 문서 정확. config off 경로 유닛테스트 15케이스 추가(226테스트) + +- organization config 하위호환·클레임 정합, 백업코드 경고 정확, 문서 정확성. 마이그레이션 생성만. 게이트. + +--- + +## 최종 게이트 & 커밋 (STEP 7-8) + +- [x] 전체(2026-07-06): `bun run test`(통합 포함) · `check` · `lint` · `build` · `typecheck` · `db:check`(4방언) · `db:generate:all` no-op — 전부 그린 +- [x] 마이그레이션 생성물: users 인덱스(0025/0008)·email_change_tokens+pendingEmail(0026/0009)·organizationClaimConfig(0027/0010) — **적용은 사용자 몫** +- [ ] 커밋 승인 게이트 → 페이즈별 원자 커밋(공유 파일은 논리 배치). 푸시 금지. From 18433b965cc966d44bbdffaf3e8204467808140a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 09:12:00 +0000 Subject: [PATCH 26/26] =?UTF-8?q?fix(ci):=20=EB=A0=88=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=EB=B0=8B=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=83=80?= =?UTF-8?q?=EC=9E=84=EC=95=84=EC=9B=83=20=EC=83=81=ED=96=A5=20+=20gitleaks?= =?UTF-8?q?=20=EC=98=A4=ED=83=90=203=EA=B1=B4=20ignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - login-mfa 레이트리밋 테스트는 11회 순차 로그인마다 scrypt 검증 + 실패 타이밍 균등화 지연이 누적되어 CI 러너에서 기본 5s 를 초과하므로 per-test timeout 30s 로 상향 - gitleaks generic-api-key 오탐 3건(.gitleaksignore): 테스트 시드 비밀번호 · GCM 도메인 분리 라벨 · 테스트 상수 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0128wqZti9tnyoa4dyiX29tT --- .gitleaksignore | 9 +++++++++ test/integration/login-mfa.test.ts | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.gitleaksignore b/.gitleaksignore index 1712e11..ba2ba26 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -1,2 +1,11 @@ # False positive: "pbkdf2/argon2id" 는 credentials.secret 컬럼이 어떤 해시 알고리즘 형식을 저장하는지 설명하는 문자열일 뿐, 실제 시크릿이 아님 02bc8b64f32a78d60cc39c46acfd6d49b384cbbd:src/lib/server/db/schema.ts:generic-api-key:62 + +# False positive: 테스트 시드용 고정 비밀번호("pw-strong-123") — 실제 시크릿 아님 +57e8e95ccc4038c7a631a7c3318efcb446c15c8e:test/integration/p12-logic.test.ts:generic-api-key:45 + +# False positive: AES-GCM 도메인 분리 라벨 문자열("idp-signing-key-wrap-v1") — 실제 시크릿 아님 +e83b290fba4a75c4c989434b024b589c1e256918:scripts/reencrypt-secrets.ts:generic-api-key:194 + +# False positive: 테스트 상수("wrap-secret-0123456789") — 실제 시크릿 아님 +d48992c3dd491706b9795d1e2ae3de3d234eadcf:test/unit/crypto-keys.test.ts:generic-api-key:204 diff --git a/test/integration/login-mfa.test.ts b/test/integration/login-mfa.test.ts index d38b419..43c81f8 100644 --- a/test/integration/login-mfa.test.ts +++ b/test/integration/login-mfa.test.ts @@ -137,7 +137,8 @@ describe("로그인 → MFA 체이닝", () => { expect(rows[0].amr).toContain("swk"); }); - it("로그인 IP 레이트리밋: 임계(10회/15분) 초과 시 429 로 차단한다", async () => { + // 11회 순차 로그인은 시도마다 scrypt 검증 + 실패 타이밍 균등화 지연이 누적되므로 기본 5s 로는 부족하다. + it("로그인 IP 레이트리밋: 임계(10회/15분) 초과 시 429 로 차단한다", { timeout: 30_000 }, async () => { const jar = makeCookieJar(); // 잘못된 비밀번호로 10회 시도(모두 400), 11회째는 IP 레이트리밋으로 429. for (let i = 0; i < 10; i++) {