From 130d868ac0ccb956e50aa18dc185e743f51a5ea5 Mon Sep 17 00:00:00 2001 From: Henry Jang Date: Fri, 17 Apr 2026 20:44:54 +0900 Subject: [PATCH] Refactor code for improved readability and consistency - Updated import statements to use multiline format for better clarity in multiple files. - Reformatted long lines and complex expressions for improved readability in various routes. - Enhanced code structure in OIDC and SAML routes for better maintainability. - Adjusted TypeScript configuration for improved file organization. --- .github/workflows/deploy.yml | 61 ----- .prettierrc | 2 +- .vscode/extensions.json | 7 +- docs/audit1.md | 4 +- drizzle.config.ts | 5 +- scripts/setup.ts | 242 +++++++++++++---- src/hooks.server.ts | 23 +- src/lib/server/audit/index.ts | 5 +- src/lib/server/auth/bootstrap.ts | 44 ++- src/lib/server/auth/constants.ts | 21 +- src/lib/server/auth/guards.ts | 6 +- src/lib/server/auth/mfa.ts | 18 +- src/lib/server/auth/password.ts | 34 ++- src/lib/server/auth/session.ts | 16 +- src/lib/server/auth/totp.ts | 63 ++++- src/lib/server/auth/users.ts | 24 +- src/lib/server/auth/webauthn.ts | 93 +++++-- src/lib/server/crypto/keys.ts | 132 +++++++-- src/lib/server/db/schema.ts | 140 ++++++++-- src/lib/server/ldap/auth.ts | 18 +- src/lib/server/ldap/client.ts | 127 +++++---- src/lib/server/ldap/provision.ts | 36 ++- src/lib/server/oidc/client.ts | 23 +- src/lib/server/oidc/grant.ts | 34 ++- src/lib/server/oidc/pkce.ts | 6 +- src/lib/server/org/membership.ts | 15 +- src/lib/server/ratelimit/index.ts | 6 +- src/lib/server/saml/metadata.ts | 20 +- src/lib/server/saml/parse-authn-request.ts | 36 ++- src/lib/server/saml/response.ts | 67 +++-- src/lib/server/saml/sp.ts | 14 +- src/routes/(auth)/login/+page.server.ts | 29 +- src/routes/(auth)/login/+page.svelte | 33 ++- src/routes/(auth)/mfa/+page.server.ts | 43 ++- src/routes/(auth)/mfa/+page.svelte | 27 +- src/routes/+page.svelte | 7 +- .../openid-configuration/+server.ts | 23 +- src/routes/account/mfa/+page.server.ts | 122 +++++++-- src/routes/account/mfa/+page.svelte | 93 +++++-- src/routes/account/passkeys/+page.server.ts | 17 +- src/routes/account/passkeys/+page.svelte | 47 +++- src/routes/account/profile/+page.svelte | 96 +++++-- src/routes/admin/+layout.server.ts | 5 +- src/routes/admin/+layout.svelte | 17 +- src/routes/admin/+page.server.ts | 22 +- src/routes/admin/+page.svelte | 8 +- src/routes/admin/audit/+page.server.ts | 13 +- src/routes/admin/audit/+page.svelte | 80 ++++-- src/routes/admin/departments/+page.server.ts | 17 +- src/routes/admin/departments/+page.svelte | 142 +++++++--- .../admin/ldap-providers/+page.server.ts | 15 +- src/routes/admin/ldap-providers/+page.svelte | 253 ++++++++++++++---- src/routes/admin/login/+page.server.ts | 10 +- src/routes/admin/login/+page.svelte | 36 ++- src/routes/admin/oidc-clients/+page.server.ts | 12 +- src/routes/admin/oidc-clients/+page.svelte | 182 ++++++++++--- src/routes/admin/parts/+page.svelte | 143 +++++++--- src/routes/admin/positions/+page.server.ts | 10 +- src/routes/admin/positions/+page.svelte | 97 +++++-- src/routes/admin/saml-sps/+page.server.ts | 22 +- src/routes/admin/saml-sps/+page.svelte | 241 +++++++++++++---- src/routes/admin/signing-keys/+page.server.ts | 14 +- src/routes/admin/signing-keys/+page.svelte | 59 +++- src/routes/admin/teams/+page.svelte | 138 +++++++--- src/routes/admin/users/+page.server.ts | 5 +- src/routes/admin/users/+page.svelte | 147 +++++++--- src/routes/admin/users/[id]/+page.server.ts | 33 ++- src/routes/admin/users/[id]/+page.svelte | 188 +++++++++---- .../webauthn/authenticate/options/+server.ts | 6 +- .../webauthn/authenticate/verify/+server.ts | 29 +- .../api/webauthn/register/options/+server.ts | 21 +- .../api/webauthn/register/verify/+server.ts | 8 +- src/routes/oidc/authorize/+server.ts | 21 +- src/routes/oidc/end-session/+server.ts | 20 +- src/routes/oidc/token/+server.ts | 6 +- src/routes/oidc/userinfo/+server.ts | 12 +- src/routes/poc/+page.svelte | 12 +- src/routes/poc/rs256/+server.ts | 15 +- src/routes/poc/saml-sign/+server.ts | 5 +- src/routes/saml/slo/+server.ts | 5 +- src/routes/saml/sso/+server.ts | 49 +++- svelte.config.js | 3 +- tsconfig.json | 5 +- 83 files changed, 3112 insertions(+), 893 deletions(-) delete mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 2b74a68..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Deploy - -on: - push: - branches: [main] - -jobs: - check-changes: - name: Check Changed Files - runs-on: ubuntu-latest - permissions: - contents: read - outputs: - build-affected: ${{ steps.filter.outputs.build-affected }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Filter paths - uses: dorny/paths-filter@v3 - id: filter - with: - filters: | - build-affected: - - "src/**" - - "static/**" - - "package.json" - - "bun.lock" - - "vite.config.*" - - "svelte.config.*" - - "tsconfig.json" - - "wrangler.toml" - - "wrangler.json" - - "drizzle.config.*" - - "eslint.config.*" - - ".prettierrc*" - - deploy: - name: Build & Deploy to Cloudflare Workers - runs-on: ubuntu-latest - needs: check-changes - if: needs.check-changes.outputs.build-affected == 'true' - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - - - name: Install dependencies - run: bun install - - - name: Build - run: bun run build - - - name: Deploy - run: bun run deploy - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.prettierrc b/.prettierrc index ecf8f6b..70018b8 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,7 +3,7 @@ "tabWidth": 4, "singleQuote": false, "trailingComma": "all", - "printWidth": 200, + "printWidth": 100, "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], "overrides": [ { diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 61ce9aa..dd687d1 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,8 @@ { - "recommendations": ["svelte.svelte-vscode", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "bradlc.vscode-tailwindcss"] + "recommendations": [ + "svelte.svelte-vscode", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "bradlc.vscode-tailwindcss" + ] } diff --git a/docs/audit1.md b/docs/audit1.md index ce3b237..cdc2fee 100644 --- a/docs/audit1.md +++ b/docs/audit1.md @@ -113,7 +113,9 @@ AuthnRequest에서 **AssertionConsumerServiceURL을 받아서 바로 사용**. ### 🔥 HTML Injection → XSS (RelayState) ```typescript -const relayStateInput = relayState ? `` : ""; +const relayStateInput = relayState + ? `` + : ""; ``` `"`만 escape하고 `<`, `>`, `&`는 그대로. `RelayState=abc">`로 주면 `"`는 escape되지만, 근데 input value 안이라 ... 확인하자. `value="abc">

직급 관리

-
@@ -23,7 +28,8 @@ const createErr = $derived((form as { create?: boolean; error?: string } | null)

새 직급 추가

{#if createErr} -

+

{createErr}

{/if} @@ -37,7 +43,8 @@ const createErr = $derived((form as { create?: boolean; error?: string } | null) }} class="grid grid-cols-1 gap-3 sm:grid-cols-3">
- +
- +
- - + +
- +
@@ -70,15 +87,26 @@ const createErr = $derived((form as { create?: boolean; error?: string } | null) - - - - + + + + {#if data.positions.length === 0} - + {:else} {#each data.positions as pos (pos.id)} @@ -94,28 +122,55 @@ const createErr = $derived((form as { create?: boolean; error?: string } | null) }} class="flex flex-wrap items-center gap-2"> - - - - - + + + + + {:else} - + {/if}
직급명코드레벨작업직급명코드레벨작업
등록된 직급이 없습니다.
등록된 직급이 없습니다.
{pos.name}{pos.name} {pos.code ?? "-"} {pos.level}
- +
diff --git a/src/routes/admin/saml-sps/+page.server.ts b/src/routes/admin/saml-sps/+page.server.ts index fa145a5..6786dad 100644 --- a/src/routes/admin/saml-sps/+page.server.ts +++ b/src/routes/admin/saml-sps/+page.server.ts @@ -32,7 +32,19 @@ export const load: PageServerLoad = async ({ locals }) => { return { sps: rows }; }; -const ATTRIBUTE_KEYS = ["email", "username", "displayName", "givenName", "familyName", "surName", "phoneNumber", "department", "team", "jobTitle", "position"] as const; +const ATTRIBUTE_KEYS = [ + "email", + "username", + "displayName", + "givenName", + "familyName", + "surName", + "phoneNumber", + "department", + "team", + "jobTitle", + "position", +] as const; function parseAllowedAttributes(raw: string): string | null { const trimmed = raw.trim(); @@ -41,7 +53,9 @@ function parseAllowedAttributes(raw: string): string | null { .split(",") .map((s) => s.trim()) .filter((s) => s.length > 0) - .filter((s): s is (typeof ATTRIBUTE_KEYS)[number] => (ATTRIBUTE_KEYS as readonly string[]).includes(s)); + .filter((s): s is (typeof ATTRIBUTE_KEYS)[number] => + (ATTRIBUTE_KEYS as readonly string[]).includes(s), + ); if (parts.length === 0) return null; return JSON.stringify([...new Set(parts)]); } @@ -57,7 +71,9 @@ export const actions: Actions = { const entityId = String(fd.get("entityId") ?? "").trim(); const acsUrl = String(fd.get("acsUrl") ?? "").trim(); const sloUrl = String(fd.get("sloUrl") ?? "").trim(); - const nameIdFormat = String(fd.get("nameIdFormat") ?? "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress").trim(); + const nameIdFormat = String( + fd.get("nameIdFormat") ?? "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + ).trim(); const cert = String(fd.get("cert") ?? "").trim(); const signAssertion = fd.get("signAssertion") === "true"; const signResponse = fd.get("signResponse") === "true"; diff --git a/src/routes/admin/saml-sps/+page.svelte b/src/routes/admin/saml-sps/+page.svelte index 59e9a3a..47df3cf 100644 --- a/src/routes/admin/saml-sps/+page.svelte +++ b/src/routes/admin/saml-sps/+page.svelte @@ -62,7 +62,9 @@ function parseSpMetadata() { if (nameIdEl?.textContent) createNameIdFormat = nameIdEl.textContent.trim(); // 서명용 인증서 - const certEl = doc.querySelector("KeyDescriptor[use='signing'] X509Certificate") ?? doc.querySelector("X509Certificate"); + const certEl = + doc.querySelector("KeyDescriptor[use='signing'] X509Certificate") ?? + doc.querySelector("X509Certificate"); if (certEl?.textContent) { const raw = certEl.textContent.trim().replace(/\s/g, ""); const lines = raw.match(/.{1,64}/g)?.join("\n") ?? raw; @@ -97,7 +99,11 @@ function resetCreateForm() { metaParseError = null; } -const createErr = $derived((form as { create?: boolean; error?: string } | null)?.create ? ((form as { error?: string } | null)?.error ?? null) : null); +const createErr = $derived( + (form as { create?: boolean; error?: string } | null)?.create + ? ((form as { error?: string } | null)?.error ?? null) + : null, +); const globalErr = $derived(createErr ? null : ((form as { error?: string } | null)?.error ?? null)); function formatAllowedAttributes(raw: string | null): string { @@ -147,10 +153,12 @@ const NAME_ID_OPTIONS = [

- SP 메타데이터 XML로 자동 입력 (선택) + SP 메타데이터 XML로 자동 입력 (선택)

{#if metaParseError} -
+
{metaParseError}
{/if} @@ -158,12 +166,19 @@ const NAME_ID_OPTIONS = [ bind:value={metadataXml} rows="4" placeholder=" 를 붙여넣으세요" - class="block w-full rounded-md border border-gray-300 px-3 py-1.5 font-mono text-xs focus:border-blue-500 focus:outline-none"> - + class="block w-full rounded-md border border-gray-300 px-3 py-1.5 font-mono text-xs focus:border-blue-500 focus:outline-none" + > +
{#if createErr} -
+
{createErr}
{/if} @@ -181,7 +196,8 @@ const NAME_ID_OPTIONS = [ }} class="grid grid-cols-1 gap-3 sm:grid-cols-2">
- +
- +
- +
- +
- + - + +
- - + +
- - + +
- + + class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-1.5 font-mono text-xs focus:border-blue-500 focus:outline-none" + >
- +
- +
@@ -277,40 +327,70 @@ const NAME_ID_OPTIONS = [ - - - - - - + + + + + + {#if data.sps.length === 0} - + {:else} {#each data.sps as sp (sp.id)} - + - + diff --git a/src/routes/admin/signing-keys/+page.server.ts b/src/routes/admin/signing-keys/+page.server.ts index 778cf2c..bc42c17 100644 --- a/src/routes/admin/signing-keys/+page.server.ts +++ b/src/routes/admin/signing-keys/+page.server.ts @@ -5,7 +5,11 @@ import { requireDbContext } from "$lib/server/auth/guards"; import { getRuntimeConfig } from "$lib/server/auth/runtime"; import { recordAuditEvent, getRequestMetadata } from "$lib/server/audit/index"; import { signingKeys } from "$lib/server/db/schema"; -import { generateRsaSigningKey, wrapPrivateKey, generateSelfSignedCert } from "$lib/server/crypto/keys"; +import { + generateRsaSigningKey, + wrapPrivateKey, + generateSelfSignedCert, +} from "$lib/server/crypto/keys"; export const load: PageServerLoad = async ({ locals }) => { const { db, tenant } = requireDbContext(locals); @@ -47,7 +51,13 @@ export const actions: Actions = { await db .update(signingKeys) .set({ active: false, rotatedAt: new Date() }) - .where(and(eq(signingKeys.tenantId, tenant.id), eq(signingKeys.active, true), isNull(signingKeys.rotatedAt))); + .where( + and( + eq(signingKeys.tenantId, tenant.id), + eq(signingKeys.active, true), + isNull(signingKeys.rotatedAt), + ), + ); // 새 키 생성 const { kid, publicKey, privateKey, publicJwk } = await generateRsaSigningKey(); diff --git a/src/routes/admin/signing-keys/+page.svelte b/src/routes/admin/signing-keys/+page.svelte index 262f5a5..0d1d3a6 100644 --- a/src/routes/admin/signing-keys/+page.svelte +++ b/src/routes/admin/signing-keys/+page.svelte @@ -23,7 +23,12 @@ const globalErr = $derived((form as { error?: string } | null)?.error ?? null); type="submit" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-blue-700" onclick={(e) => { - if (!confirm("새 키를 생성하고 기존 활성 키를 비활성화합니다.\n계속하시겠습니까?")) e.preventDefault(); + if ( + !confirm( + "새 키를 생성하고 기존 활성 키를 비활성화합니다.\n계속하시겠습니까?", + ) + ) + e.preventDefault(); }}> 키 로테이션 @@ -49,40 +54,66 @@ const globalErr = $derived((form as { error?: string } | null)?.error ?? null);
이름 / Entity IDACS URL서명상태생성작업이름 / Entity IDACS URL서명상태생성작업
등록된 SAML SP 가 없습니다.등록된 SAML SP 가 없습니다.

{sp.name}

-

{sp.entityId}

+

+ {sp.entityId} +

{sp.acsUrl}{sp.acsUrl} - {#if sp.signAssertion}Assertion{/if} - {#if sp.signResponse}Response{/if} + {#if sp.signAssertion}Assertion{/if} + {#if sp.signResponse}Response{/if} - + {sp.enabled ? "활성" : "비활성"} {dateFormatter.format(sp.createdAt)}{dateFormatter.format(sp.createdAt)}
-
@@ -319,7 +399,8 @@ const NAME_ID_OPTIONS = [ type="submit" class="text-xs text-red-400 hover:text-red-600" onclick={(e) => { - if (!confirm("SP를 삭제하시겠습니까?")) e.preventDefault(); + if (!confirm("SP를 삭제하시겠습니까?")) + e.preventDefault(); }}> 삭제 @@ -343,7 +424,10 @@ const NAME_ID_OPTIONS = [ class="grid grid-cols-1 gap-3 sm:grid-cols-2">
- +
- +
- +
- + - +
- - + +
- +
- - + +
- +
-
- - +
- - - - - - - - + + + + + + + + {#if data.keys.length === 0} - + {:else} {#each data.keys as key (key.id)} - + - + diff --git a/src/routes/admin/teams/+page.svelte b/src/routes/admin/teams/+page.svelte index 1d05599..489a12c 100644 --- a/src/routes/admin/teams/+page.svelte +++ b/src/routes/admin/teams/+page.svelte @@ -8,7 +8,9 @@ let showCreate = $state(false); let editId = $state(null); const err = $derived((form as { error?: string } | null)?.error ?? null); -const createErr = $derived((form as { create?: boolean; error?: string } | null)?.create ? err : null); +const createErr = $derived( + (form as { create?: boolean; error?: string } | null)?.create ? err : null, +); const STATUS_COLOR: Record = { active: "bg-green-100 text-green-700", @@ -20,7 +22,10 @@ const STATUS_LABEL: Record = { active: "활성", inactive: "비

팀 관리

-
@@ -29,7 +34,8 @@ const STATUS_LABEL: Record = { active: "활성", inactive: "비

새 팀 추가

{#if createErr} -

+

{createErr}

{/if} @@ -43,7 +49,8 @@ const STATUS_LABEL: Record = { active: "활성", inactive: "비 }} class="grid grid-cols-1 gap-3 sm:grid-cols-2">
- + = { active: "활성", inactive: "비 class="mt-1 w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:outline-none" />
- + = { active: "활성", inactive: "비 class="mt-1 w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:outline-none" />
- - {#each data.allDepts as dept (dept.id)} @@ -71,34 +83,57 @@ const STATUS_LABEL: Record = { active: "활성", inactive: "비
- - + +
- +
{/if} {#if err && !createErr} -

{err}

+

+ {err} +

{/if}
KID알고리즘용도인증서상태생성일로테이션만료일KID알고리즘용도인증서상태생성일로테이션만료일
등록된 서명 키가 없습니다. 키 로테이션 버튼을 눌러 첫 번째 키를 생성하세요. + 등록된 서명 키가 없습니다. 키 로테이션 버튼을 눌러 첫 번째 키를 + 생성하세요. +
{key.kid}{key.kid} {key.alg} {key.use ?? "sig"} {#if key.hasCert} - 있음 + 있음 {:else} 없음 {/if} - + {key.active ? "활성" : "비활성"} {dateFormatter.format(key.createdAt)}{dateFormatter.format(key.createdAt)} {key.rotatedAt ? dateTimeFormatter.format(key.rotatedAt) : "—"}
- - - - - + + + + + {#if data.teams.length === 0} - + {:else} {#each data.teams as team (team.id)} @@ -114,42 +149,83 @@ const STATUS_LABEL: Record = { active: "활성", inactive: "비 }} class="grid grid-cols-2 gap-2 sm:grid-cols-4"> - - - + + - + +
- - + +
{:else} -
+ - + {/if}
팀명코드소속 부서상태작업팀명코드소속 부서상태작업
등록된 팀이 없습니다.
등록된 팀이 없습니다.
{team.name}{team.name} {team.code ?? "-"}{team.departmentName ?? "-"}{team.departmentName ?? "-"} - {STATUS_LABEL[team.status]} + {STATUS_LABEL[team.status]}
- +
diff --git a/src/routes/admin/users/+page.server.ts b/src/routes/admin/users/+page.server.ts index 230d385..33a07ef 100644 --- a/src/routes/admin/users/+page.server.ts +++ b/src/routes/admin/users/+page.server.ts @@ -221,7 +221,10 @@ export const actions: Actions = { .limit(1); if (existing) { - await db.update(credentials).set({ secret: hashed }).where(eq(credentials.id, existing.id)); + await db + .update(credentials) + .set({ secret: hashed }) + .where(eq(credentials.id, existing.id)); } else { await db.insert(credentials).values({ id: crypto.randomUUID(), diff --git a/src/routes/admin/users/+page.svelte b/src/routes/admin/users/+page.svelte index 4b311fc..a98eb8d 100644 --- a/src/routes/admin/users/+page.svelte +++ b/src/routes/admin/users/+page.svelte @@ -14,8 +14,12 @@ let showCreate = $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 resetErr = $derived((form as { resetPassword?: boolean; error?: string } | null)?.resetPassword ? formErr : null); +const createErr = $derived( + (form as { create?: boolean; error?: string } | null)?.create ? formErr : null, +); +const resetErr = $derived( + (form as { resetPassword?: boolean; error?: string } | null)?.resetPassword ? formErr : null, +); const STATUS_LABEL: Record = { active: "활성", @@ -37,7 +41,10 @@ const STATUS_COLOR: Record = {

사용자 관리

-
@@ -48,7 +55,8 @@ const STATUS_COLOR: Record = {

새 사용자 추가

{#if createErr} -
+
{createErr}
{/if} @@ -64,15 +72,27 @@ const STATUS_COLOR: Record = { }} class="grid grid-cols-1 gap-3 sm:grid-cols-2">
- - + +
- - + +
- + = { class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:outline-none" />
- -
- + = { class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:outline-none" />
- +
@@ -115,29 +144,47 @@ const STATUS_COLOR: Record = { - - - - - - + + + + + + {#if data.users.length === 0} - + {:else} {#each data.users as user (user.id)} - + @@ -156,14 +208,22 @@ const STATUS_COLOR: Record = { - + diff --git a/src/routes/admin/users/[id]/+page.server.ts b/src/routes/admin/users/[id]/+page.server.ts index 3096511..d3c0c44 100644 --- a/src/routes/admin/users/[id]/+page.server.ts +++ b/src/routes/admin/users/[id]/+page.server.ts @@ -2,7 +2,16 @@ import { fail, error } from "@sveltejs/kit"; import { and, asc, eq, isNull } from "drizzle-orm"; import type { Actions, PageServerLoad } from "./$types"; import { requireDbContext } from "$lib/server/auth/guards"; -import { departments, parts, positions, teams, userDepartments, userParts, userTeams, users } from "$lib/server/db/schema"; +import { + departments, + parts, + positions, + teams, + userDepartments, + userParts, + userTeams, + users, +} from "$lib/server/db/schema"; export const load: PageServerLoad = async ({ locals, params }) => { const { db, tenant } = requireDbContext(locals); @@ -87,7 +96,11 @@ export const load: PageServerLoad = async ({ locals, params }) => { .where(and(eq(parts.tenantId, tenant.id), eq(parts.status, "active"))) .orderBy(asc(teams.name), asc(parts.name)); - const allPositions = await db.select({ id: positions.id, name: positions.name, level: positions.level }).from(positions).where(eq(positions.tenantId, tenant.id)).orderBy(asc(positions.level)); + const allPositions = await db + .select({ id: positions.id, name: positions.name, level: positions.level }) + .from(positions) + .where(eq(positions.tenantId, tenant.id)) + .orderBy(asc(positions.level)); return { user, @@ -185,7 +198,9 @@ export const actions: Actions = { if (!pos) return fail(404, { error: "직책을 찾을 수 없습니다." }); } - await db.insert(userDepartments).values({ tenantId: tenant.id, userId, departmentId, positionId, jobTitle, isPrimary }); + await db + .insert(userDepartments) + .values({ tenantId: tenant.id, userId, departmentId, positionId, jobTitle, isPrimary }); return { addedDept: true }; }, @@ -199,7 +214,9 @@ export const actions: Actions = { await db .update(userDepartments) .set({ endedAt: new Date() }) - .where(and(eq(userDepartments.id, membershipId), eq(userDepartments.tenantId, tenant.id))); + .where( + and(eq(userDepartments.id, membershipId), eq(userDepartments.tenantId, tenant.id)), + ); return { removedDept: true }; }, @@ -228,7 +245,9 @@ export const actions: Actions = { .limit(1); if (!team) return fail(404, { error: "팀을 찾을 수 없습니다." }); - await db.insert(userTeams).values({ tenantId: tenant.id, userId, teamId, jobTitle, isPrimary }); + await db + .insert(userTeams) + .values({ tenantId: tenant.id, userId, teamId, jobTitle, isPrimary }); return { addedTeam: true }; }, @@ -271,7 +290,9 @@ export const actions: Actions = { .limit(1); if (!part) return fail(404, { error: "파트를 찾을 수 없습니다." }); - await db.insert(userParts).values({ tenantId: tenant.id, userId, partId, jobTitle, isPrimary }); + await db + .insert(userParts) + .values({ tenantId: tenant.id, userId, partId, jobTitle, isPrimary }); return { addedPart: true }; }, diff --git a/src/routes/admin/users/[id]/+page.svelte b/src/routes/admin/users/[id]/+page.svelte index b0c2ba6..22947e7 100644 --- a/src/routes/admin/users/[id]/+page.svelte +++ b/src/routes/admin/users/[id]/+page.svelte @@ -34,7 +34,8 @@ const TIMEZONE_OPTIONS = [
- ← 목록 + ← 목록

{data.user.displayName ?? data.user.email}

{data.user.email}
@@ -45,7 +46,10 @@ const TIMEZONE_OPTIONS = [
{/if} {#if updated} -
저장되었습니다.
+
+ 저장되었습니다. +
{/if} @@ -54,7 +58,8 @@ const TIMEZONE_OPTIONS = [
- +
- +
- +
- +
- +
-
- {#each LOCALE_OPTIONS as opt (opt.value)} - + {/each}
- - {#each TIMEZONE_OPTIONS as opt (opt.value)} - + {/each}
- +
- + + +
- +
@@ -153,14 +188,20 @@ const TIMEZONE_OPTIONS = [
{m.departmentName} - {#if m.isPrimary}주소속{/if} - {#if m.positionName}{m.positionName}{/if} + {#if m.isPrimary}주소속{/if} + {#if m.positionName}{m.positionName}{/if} {#if m.jobTitle}/ {m.jobTitle}{/if} - {dateFormatter.format(m.startedAt)} ~ + {dateFormatter.format(m.startedAt)} ~
- +
{/each} @@ -170,26 +211,42 @@ const TIMEZONE_OPTIONS = [ {/if} -
- {#each data.allDepts as d (d.id)} {/each} - {#each data.allPositions as p (p.id)} {/each} - +
- +
@@ -204,14 +261,21 @@ const TIMEZONE_OPTIONS = [
{m.teamName} - {#if m.departmentName}({m.departmentName}){/if} - {#if m.isPrimary}주소속{/if} + {#if m.departmentName}({m.departmentName}){/if} + {#if m.isPrimary}주소속{/if} {#if m.jobTitle}/ {m.jobTitle}{/if} - {dateFormatter.format(m.startedAt)} ~ + {dateFormatter.format(m.startedAt)} ~
- +
{/each} @@ -220,20 +284,34 @@ const TIMEZONE_OPTIONS = [

소속된 팀이 없습니다.

{/if} -
- {#each data.allTeams as t (t.id)} {/each} - +
- +
@@ -248,14 +326,20 @@ const TIMEZONE_OPTIONS = [
{m.partName} - {#if m.teamName}({m.teamName}){/if} - {#if m.isPrimary}주소속{/if} + {#if m.teamName}({m.teamName}){/if} + {#if m.isPrimary}주소속{/if} {#if m.jobTitle}/ {m.jobTitle}{/if} - {dateFormatter.format(m.startedAt)} ~ + {dateFormatter.format(m.startedAt)} ~
- +
{/each} @@ -264,20 +348,34 @@ const TIMEZONE_OPTIONS = [

소속된 파트가 없습니다.

{/if} -
- {#each data.allParts as p (p.id)} {/each} - +
- +
diff --git a/src/routes/api/webauthn/authenticate/options/+server.ts b/src/routes/api/webauthn/authenticate/options/+server.ts index 4f2d52d..b2efe49 100644 --- a/src/routes/api/webauthn/authenticate/options/+server.ts +++ b/src/routes/api/webauthn/authenticate/options/+server.ts @@ -1,7 +1,11 @@ import { json } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; import { requireDbContext } from "$lib/server/auth/guards"; -import { buildAuthenticationOptions, getWebAuthnConfig, saveChallenge } from "$lib/server/auth/webauthn"; +import { + buildAuthenticationOptions, + getWebAuthnConfig, + saveChallenge, +} from "$lib/server/auth/webauthn"; export const POST: RequestHandler = async ({ url, locals }) => { const { db } = requireDbContext(locals); diff --git a/src/routes/api/webauthn/authenticate/verify/+server.ts b/src/routes/api/webauthn/authenticate/verify/+server.ts index f488ecf..c46aa73 100644 --- a/src/routes/api/webauthn/authenticate/verify/+server.ts +++ b/src/routes/api/webauthn/authenticate/verify/+server.ts @@ -6,7 +6,11 @@ import { recordAuditEvent, getRequestMetadata } from "$lib/server/audit/index"; import { createSessionRecord, setSessionCookie } from "$lib/server/auth/session"; import { AMR_WEBAUTHN, amrToAcr } from "$lib/server/auth/constants"; import { checkRateLimit } from "$lib/server/ratelimit"; -import { verifyPasskeyAuthentication, consumeChallenge, getWebAuthnConfig } from "$lib/server/auth/webauthn"; +import { + verifyPasskeyAuthentication, + consumeChallenge, + getWebAuthnConfig, +} from "$lib/server/auth/webauthn"; import type { AuthenticationResponseJSON } from "$lib/server/auth/webauthn"; import { users } from "$lib/server/db/schema"; @@ -39,7 +43,9 @@ export const POST: RequestHandler = async (event) => { } // 1회용 challenge 소진 (DB 기반) - const clientChallenge = body.response?.clientDataJSON ? extractChallengeFromClientData(body.response.clientDataJSON) : null; + const clientChallenge = body.response?.clientDataJSON + ? extractChallengeFromClientData(body.response.clientDataJSON) + : null; if (!clientChallenge) { throw error(400, "인증 세션이 유효하지 않습니다."); } @@ -48,7 +54,14 @@ export const POST: RequestHandler = async (event) => { throw error(400, "인증 세션이 만료되었거나 이미 사용되었습니다."); } - const result = await verifyPasskeyAuthentication(db, body, clientChallenge, rpID, origin, tenant.id); + const result = await verifyPasskeyAuthentication( + db, + body, + clientChallenge, + rpID, + origin, + tenant.id, + ); if (!result) { const requestMetadata = getRequestMetadata(event); @@ -102,7 +115,15 @@ export const POST: RequestHandler = async (event) => { let safeRedirect: string; try { const decoded = decodeURIComponent(requested); - safeRedirect = decoded && decoded.startsWith("/") && !decoded.startsWith("//") && !decoded.includes("\\") ? requested : user.role === "admin" ? "/admin" : "/"; + safeRedirect = + decoded && + decoded.startsWith("/") && + !decoded.startsWith("//") && + !decoded.includes("\\") + ? requested + : user.role === "admin" + ? "/admin" + : "/"; } catch { safeRedirect = user.role === "admin" ? "/admin" : "/"; } diff --git a/src/routes/api/webauthn/register/options/+server.ts b/src/routes/api/webauthn/register/options/+server.ts index a419151..03fed63 100644 --- a/src/routes/api/webauthn/register/options/+server.ts +++ b/src/routes/api/webauthn/register/options/+server.ts @@ -2,7 +2,12 @@ import { json, error } from "@sveltejs/kit"; 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 { + buildRegistrationOptions, + createChallengeCookie, + getWebAuthnConfig, + WEBAUTHN_CHALLENGE_COOKIE, +} from "$lib/server/auth/webauthn"; export const POST: RequestHandler = async (event) => { const { locals, cookies, url, platform } = event; @@ -18,10 +23,20 @@ export const POST: RequestHandler = async (event) => { const { db } = requireDbContext(locals); const { rpID, rpName, origin } = getWebAuthnConfig(url); - const options = await buildRegistrationOptions(db, locals.user.id, locals.user.email, locals.user.displayName, rpID, rpName); + const options = await buildRegistrationOptions( + db, + locals.user.id, + locals.user.email, + locals.user.displayName, + rpID, + rpName, + ); // challenge 를 HMAC-서명 쿠키에 저장 - const cookieValue = await createChallengeCookie({ challenge: options.challenge, type: "register", userId: locals.user.id }, config.signingKeySecret); + const cookieValue = await createChallengeCookie( + { challenge: options.challenge, type: "register", userId: locals.user.id }, + config.signingKeySecret, + ); cookies.set(WEBAUTHN_CHALLENGE_COOKIE, cookieValue, { path: "/", diff --git a/src/routes/api/webauthn/register/verify/+server.ts b/src/routes/api/webauthn/register/verify/+server.ts index c489905..80aab48 100644 --- a/src/routes/api/webauthn/register/verify/+server.ts +++ b/src/routes/api/webauthn/register/verify/+server.ts @@ -3,7 +3,13 @@ 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 { verifyChallengeCookie, verifyRegistrationResponse, savePasskey, getWebAuthnConfig, WEBAUTHN_CHALLENGE_COOKIE } from "$lib/server/auth/webauthn"; +import { + verifyChallengeCookie, + verifyRegistrationResponse, + savePasskey, + getWebAuthnConfig, + WEBAUTHN_CHALLENGE_COOKIE, +} from "$lib/server/auth/webauthn"; import type { RegistrationResponseJSON } from "$lib/server/auth/webauthn"; export const POST: RequestHandler = async (event) => { diff --git a/src/routes/oidc/authorize/+server.ts b/src/routes/oidc/authorize/+server.ts index d618e11..8c50523 100644 --- a/src/routes/oidc/authorize/+server.ts +++ b/src/routes/oidc/authorize/+server.ts @@ -7,7 +7,12 @@ import { createGrant } from "$lib/server/oidc/grant"; import { checkRateLimit } from "$lib/server/ratelimit"; /** redirect_uri 가 확정된 이후에만 사용. 그 전 오류는 throw error() 로 직접 응답. */ -function authRedirectError(redirectUri: string, errorCode: string, description: string, state?: string | null): never { +function authRedirectError( + redirectUri: string, + errorCode: string, + description: string, + state?: string | null, +): never { const dest = new URL(redirectUri); dest.searchParams.set("error", errorCode); dest.searchParams.set("error_description", description); @@ -58,10 +63,20 @@ export const GET: RequestHandler = async (event) => { // PKCE 검증 if (client.requirePkce) { if (!codeChallenge) { - authRedirectError(redirectUri, "invalid_request", "PKCE code_challenge 가 필요합니다.", state); + authRedirectError( + redirectUri, + "invalid_request", + "PKCE code_challenge 가 필요합니다.", + state, + ); } if (codeChallengeMethod !== "S256") { - authRedirectError(redirectUri, "invalid_request", "code_challenge_method=S256 만 지원합니다.", state); + authRedirectError( + redirectUri, + "invalid_request", + "code_challenge_method=S256 만 지원합니다.", + state, + ); } } diff --git a/src/routes/oidc/end-session/+server.ts b/src/routes/oidc/end-session/+server.ts index a3fd4b9..1f99ba1 100644 --- a/src/routes/oidc/end-session/+server.ts +++ b/src/routes/oidc/end-session/+server.ts @@ -4,7 +4,11 @@ import { and, eq } from "drizzle-orm"; import { oidcClients } from "$lib/server/db/schema"; import { clearSessionCookie, revokeSession } from "$lib/server/auth/session"; -async function handleEndSession(locals: App.Locals, url: URL, cookies: Parameters[0]["cookies"]): Promise { +async function handleEndSession( + locals: App.Locals, + url: URL, + cookies: Parameters[0]["cookies"], +): Promise { const postLogoutRedirectUri = url.searchParams.get("post_logout_redirect_uri"); const clientId = url.searchParams.get("client_id"); @@ -19,7 +23,13 @@ async function handleEndSession(locals: App.Locals, url: URL, cookies: Parameter const [client] = await locals.db .select({ postLogoutRedirectUris: oidcClients.postLogoutRedirectUris }) .from(oidcClients) - .where(and(eq(oidcClients.tenantId, locals.tenant.id), eq(oidcClients.clientId, clientId), eq(oidcClients.enabled, true))) + .where( + and( + eq(oidcClients.tenantId, locals.tenant.id), + eq(oidcClients.clientId, clientId), + eq(oidcClients.enabled, true), + ), + ) .limit(1); if (client?.postLogoutRedirectUris) { @@ -38,6 +48,8 @@ async function handleEndSession(locals: App.Locals, url: URL, cookies: Parameter throw redirect(302, "/"); } -export const GET: RequestHandler = ({ locals, url, cookies }) => handleEndSession(locals, url, cookies); +export const GET: RequestHandler = ({ locals, url, cookies }) => + handleEndSession(locals, url, cookies); -export const POST: RequestHandler = ({ locals, url, cookies }) => handleEndSession(locals, url, cookies); +export const POST: RequestHandler = ({ locals, url, cookies }) => + handleEndSession(locals, url, cookies); diff --git a/src/routes/oidc/token/+server.ts b/src/routes/oidc/token/+server.ts index 8cc8a97..992ed4e 100644 --- a/src/routes/oidc/token/+server.ts +++ b/src/routes/oidc/token/+server.ts @@ -106,7 +106,11 @@ export const POST: RequestHandler = async (event) => { if (!codeVerifier) { return tokenError("invalid_grant", "code_verifier 가 필요합니다."); } - const valid = await verifyPkce(grant.codeChallenge, grant.codeChallengeMethod ?? "plain", codeVerifier); + const valid = await verifyPkce( + grant.codeChallenge, + grant.codeChallengeMethod ?? "plain", + codeVerifier, + ); if (!valid) { return tokenError("invalid_grant", "code_verifier 검증에 실패했습니다."); } diff --git a/src/routes/oidc/userinfo/+server.ts b/src/routes/oidc/userinfo/+server.ts index aa012ce..6e02fdf 100644 --- a/src/routes/oidc/userinfo/+server.ts +++ b/src/routes/oidc/userinfo/+server.ts @@ -39,7 +39,13 @@ async function handleUserinfo(locals: App.Locals, request: Request): Promise

Workers 환경 PoC

-

킥오프 M0 사전 점검. 각 엔드포인트는 GET 요청 시 JSON 으로 결과를 반환.

+

+ 킥오프 M0 사전 점검. 각 엔드포인트는 GET 요청 시 JSON 으로 결과를 반환. +

    {#each pocs as p (p.route)}
  • - {resolve(p.route)} + {resolve(p.route)} {p.title} - + {p.status}
  • diff --git a/src/routes/poc/rs256/+server.ts b/src/routes/poc/rs256/+server.ts index 486af97..c6fc701 100644 --- a/src/routes/poc/rs256/+server.ts +++ b/src/routes/poc/rs256/+server.ts @@ -46,12 +46,23 @@ export const GET = async () => { }), ); const signingInput = `${header}.${payload}`; - const sigBytes = new Uint8Array(await crypto.subtle.sign({ name: "RSASSA-PKCS1-v1_5" }, keyPair.privateKey, new TextEncoder().encode(signingInput))); + const sigBytes = new Uint8Array( + await crypto.subtle.sign( + { name: "RSASSA-PKCS1-v1_5" }, + keyPair.privateKey, + new TextEncoder().encode(signingInput), + ), + ); const jwt = `${signingInput}.${b64url(sigBytes)}`; const tSign = Date.now(); // 4) 검증 - const ok = await crypto.subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, keyPair.publicKey, sigBytes, new TextEncoder().encode(signingInput)); + const ok = await crypto.subtle.verify( + { name: "RSASSA-PKCS1-v1_5" }, + keyPair.publicKey, + sigBytes, + new TextEncoder().encode(signingInput), + ); const tVerify = Date.now(); return json({ diff --git a/src/routes/poc/saml-sign/+server.ts b/src/routes/poc/saml-sign/+server.ts index ba36d07..c1384ea 100644 --- a/src/routes/poc/saml-sign/+server.ts +++ b/src/routes/poc/saml-sign/+server.ts @@ -67,7 +67,10 @@ export const GET = async () => { // 검증 라운드트립 const parsedAgain = xmldsigjs.Parse(signedString); - const sigEls = parsedAgain.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature"); + const sigEls = parsedAgain.getElementsByTagNameNS( + "http://www.w3.org/2000/09/xmldsig#", + "Signature", + ); const verifier = new xmldsigjs.SignedXml(parsedAgain); verifier.LoadXml(sigEls[0]); const verified = await verifier.Verify(); diff --git a/src/routes/saml/slo/+server.ts b/src/routes/saml/slo/+server.ts index 66d1e84..d71cb52 100644 --- a/src/routes/saml/slo/+server.ts +++ b/src/routes/saml/slo/+server.ts @@ -19,7 +19,10 @@ export const GET: RequestHandler = async (event) => { const { db, tenant } = requireDbContext(locals); if (locals.session) { - await db.update(sessions).set({ revokedAt: new Date() }).where(eq(sessions.id, locals.session.id)); + await db + .update(sessions) + .set({ revokedAt: new Date() }) + .where(eq(sessions.id, locals.session.id)); if (locals.user) { const requestMetadata = getRequestMetadata(event); diff --git a/src/routes/saml/sso/+server.ts b/src/routes/saml/sso/+server.ts index 2c95972..d60ad36 100644 --- a/src/routes/saml/sso/+server.ts +++ b/src/routes/saml/sso/+server.ts @@ -12,7 +12,10 @@ import { getRuntimeConfig } from "$lib/server/auth/runtime"; import { recordAuditEvent, getRequestMetadata } from "$lib/server/audit"; import { getActiveSigningKey } from "$lib/server/crypto/keys"; import { acrSatisfies } from "$lib/server/auth/constants"; -import { parseAuthnRequest, verifySamlRedirectSignature } from "$lib/server/saml/parse-authn-request"; +import { + parseAuthnRequest, + verifySamlRedirectSignature, +} from "$lib/server/saml/parse-authn-request"; import { buildSignedSamlErrorResponse, buildSignedSamlResponse } from "$lib/server/saml/response"; import { findSp, recordSamlSession } from "$lib/server/saml/sp"; import { getUserMembership } from "$lib/server/org/membership"; @@ -80,7 +83,9 @@ export const GET: RequestHandler = async (event) => { certPem: signingKey.certPem, privateKey: signingKey.privateKey, }); - const relayStateInput = authnRequest.relayState ? `` : ""; + const relayStateInput = authnRequest.relayState + ? `` + : ""; return new Response( `SSO 리다이렉트 중...` + `` + @@ -108,7 +113,10 @@ export const GET: RequestHandler = async (event) => { } // RequestedAuthnContext: 세션 ACR 이 SP 요구 수준을 만족하는지 검사한다. - if (authnRequest.requestedAuthnContext && !acrSatisfies(locals.session.acr, authnRequest.requestedAuthnContext)) { + if ( + authnRequest.requestedAuthnContext && + !acrSatisfies(locals.session.acr, authnRequest.requestedAuthnContext) + ) { // 세션이 issueInstant 이후에 생성됐다면 재인증을 이미 거쳤으나 ACR 이 여전히 부족한 것. // (예: MFA 미설정 사용자가 refeds/mfa 를 요구받은 경우) → NoAuthnContext 오류 반환. const isPostReauth = locals.session.createdAt >= authnRequest.issueInstant; @@ -121,7 +129,9 @@ export const GET: RequestHandler = async (event) => { certPem: signingKey.certPem, privateKey: signingKey.privateKey, }); - const relayStateInput = authnRequest.relayState ? `` : ""; + const relayStateInput = authnRequest.relayState + ? `` + : ""; return new Response( `SSO 리다이렉트 중...` + `` + @@ -155,7 +165,11 @@ export const GET: RequestHandler = async (event) => { if (sp.allowedAttributes) { try { const parsed = JSON.parse(sp.allowedAttributes) as unknown; - allowedSet = new Set(Array.isArray(parsed) ? parsed.filter((v): v is string => typeof v === "string") : DEFAULT_ALLOWED); + allowedSet = new Set( + Array.isArray(parsed) + ? parsed.filter((v): v is string => typeof v === "string") + : DEFAULT_ALLOWED, + ); } catch { allowedSet = new Set(DEFAULT_ALLOWED); } @@ -180,10 +194,15 @@ export const GET: RequestHandler = async (event) => { setAttr("phoneNumber", user.phoneNumber); // 조직 정보는 SP 가 명시적으로 허용한 경우에만 포함한다. - const wantsOrg = allowedSet.has("department") || allowedSet.has("team") || allowedSet.has("jobTitle") || allowedSet.has("position"); + const wantsOrg = + allowedSet.has("department") || + allowedSet.has("team") || + allowedSet.has("jobTitle") || + allowedSet.has("position"); if (wantsOrg) { const membership = await getUserMembership(db, user.id); - const primaryDept = membership.departments.find((d) => d.isPrimary) ?? membership.departments[0]; + const primaryDept = + membership.departments.find((d) => d.isPrimary) ?? membership.departments[0]; const primaryTeam = membership.teams.find((t) => t.isPrimary) ?? membership.teams[0]; if (primaryDept) { setAttr("department", primaryDept.name); @@ -197,7 +216,10 @@ export const GET: RequestHandler = async (event) => { // NameID 결정 const nameIdFormat = sp.nameIdFormat; - const nameId = nameIdFormat === "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" ? user.id : (user.email ?? user.id); + const nameId = + nameIdFormat === "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + ? user.id + : (user.email ?? user.id); const sessionIndex = `_si${crypto.randomUUID().replace(/-/g, "")}`; @@ -241,10 +263,17 @@ export const GET: RequestHandler = async (event) => { // HTTP-POST 바인딩: auto-submit 폼 렌더링 function htmlEscape(s: string): string { - return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); } - const relayStateInput = relayState ? `` : ""; + const relayStateInput = relayState + ? `` + : ""; const html = ` diff --git a/svelte.config.js b/svelte.config.js index 357d916..ee03cfa 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -4,7 +4,8 @@ import adapter from "@sveltejs/adapter-cloudflare"; const config = { compilerOptions: { // Force runes mode for the project, except for libraries. Can be removed in svelte 6. - runes: ({ filename }) => (filename.split(/[/\\]/).includes("node_modules") ? undefined : true), + runes: ({ filename }) => + filename.split(/[/\\]/).includes("node_modules") ? undefined : true, }, kit: { adapter: adapter(), diff --git a/tsconfig.json b/tsconfig.json index 1ab31ce..8b6ce84 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,10 @@ "moduleResolution": "bundler", "types": ["./worker-configuration.d.ts"] }, - "files": ["src/routes/.well-known/openid-configuration/+server.ts", ".svelte-kit/types/src/routes/.well-known/openid-configuration/$types.d.ts"], + "files": [ + "src/routes/.well-known/openid-configuration/+server.ts", + ".svelte-kit/types/src/routes/.well-known/openid-configuration/$types.d.ts" + ], "exclude": [ "scripts/**", "node_modules/**",
아이디 / 이메일이름역할상태생성작업아이디 / 이메일이름역할상태생성작업
등록된 사용자가 없습니다.등록된 사용자가 없습니다.
- -

{user.username ?? "-"}

+
+

+ {user.username ?? "-"} +

{user.email}

{user.displayName ?? "-"}{user.displayName ?? "-"} @@ -145,10 +192,15 @@ const STATUS_COLOR: Record = {
- + {STATUS_LABEL[user.status]} {#if STATUS_NEXT[user.status]}
- -
@@ -171,12 +231,18 @@ const STATUS_COLOR: Record = {
{dateFormatter.format(user.createdAt)}{dateFormatter.format(user.createdAt)}
- @@ -186,7 +252,8 @@ const STATUS_COLOR: Record = { type="submit" class="text-xs text-red-400 hover:text-red-600" onclick={(e) => { - if (!confirm("사용자를 삭제하시겠습니까?")) e.preventDefault(); + if (!confirm("사용자를 삭제하시겠습니까?")) + e.preventDefault(); }}> 삭제 @@ -208,7 +275,8 @@ const STATUS_COLOR: Record = { use:enhance={() => { return ({ result, update }) => { update(); - if (result.type === "success") resetPasswordUserId = null; + if (result.type === "success") + resetPasswordUserId = null; }; }} class="flex items-center gap-2"> @@ -220,8 +288,17 @@ const STATUS_COLOR: Record = { minlength="8" placeholder="새 비밀번호 (8자 이상)" class="rounded-md border border-gray-300 px-3 py-1 text-sm focus:border-blue-500 focus:outline-none" /> - - + +