diff --git a/.claude/rules/auth.md b/.claude/rules/auth.md index a287e753e..7bcac6d04 100644 --- a/.claude/rules/auth.md +++ b/.claude/rules/auth.md @@ -1,10 +1,10 @@ --- description: Authentication rules paths: - - 'src/lib/server/auth.ts' - 'src/routes/(auth)/**' - - 'src/features/auth/**' - 'src/routes/(admin)/**' + - 'src/features/auth/**' + - 'src/features/auth/server/**' - 'src/hooks.server.ts' --- @@ -31,11 +31,11 @@ Auth is a self-managed implementation (no external auth library), kept compatibl ## Key Files -- `src/lib/server/auth.ts`: `createAuthRequest` (request-scoped session handle) -- `src/lib/server/session.ts` / `src/lib/server/password.ts`: self-managed session + password crypto -- `src/features/auth/services/credentials.ts`: `registerUser` / `authenticateUser` - `src/hooks.server.ts`: Global request handler -- `src/features/auth/services/session.ts`: +- `src/features/auth/server/auth.ts`: `createAuthRequest` (request-scoped session handle) +- `src/features/auth/server/session.ts` / `src/features/auth/server/password.ts`: self-managed session + password crypto (`random.ts` provides salt/session-id generation) +- `src/features/auth/services/credentials.ts`: `registerUser` / `authenticateUser` +- `src/features/auth/services/session_guards.ts`: - `getLoggedInUser(locals, url?)` — returns logged-in user or redirects to `/login` - `ensureSessionOrRedirect(locals, url?)` — guard-only; redirects if no session - `src/features/auth/services/admin_access.ts`: diff --git a/docs/guides/architecture.md b/docs/guides/architecture.md index 68f58054c..e78ccd92f 100644 --- a/docs/guides/architecture.md +++ b/docs/guides/architecture.md @@ -91,6 +91,17 @@ src/features/ - `detail/` — 詳細ページ用コンポーネント - `shared/` — feature 内で複数ページから使うコンポーネント +**`server/` サブディレクトリ規約:** + +feature 内のサーバ専用・非サービスコード(リクエストスコープのハンドル、cookie 書き込み、 +crypto ユーティリティなど、`services/` の「純粋な値/`null` を返す framework 非依存」規約に +収まらないもの)は `features/{feature}/server/` に置く(`votes/server/`・`workbooks/server/`・ +`auth/server/` が前例)。 + +- `$lib/server/` を出るため SvelteKit のクライアント import 禁止(ビルド時エラー)は**効かない**。 + 保護は規約ベース + 推移的依存(`$lib/server/database` の import や `node:crypto` 依存)に委ねる +- 2つ以上の feature で使うサーバコードは `src/lib/server/` に昇格させる(例: `database.ts`) + ### ルート固有の `_types/` / `_utils/` ディレクトリ SvelteKit のルートディレクトリ内で、そのページ専用の型やユーティリティを colocate するために `_types/` と `_utils/` を使う。アンダースコアプレフィックスにより SvelteKit のルーティング対象外となる。 @@ -147,9 +158,9 @@ src/lib/ ├── clients/ # 外部 API クライアント(AtCoder Problems, AOJ) ├── components/ # 共通 UI コンポーネント(GradeLabel, TaskGradeList, TaskList, FormWrapper 等) ├── constants/ # アプリ定数 -├── server/ # サーバー専用コード -│ ├── auth.ts -│ ├── database.ts +├── server/ # サーバー専用の共有インフラ +│ ├── database.ts # Prisma クライアント(14+ サービスが依存) +│ ├── tasks/ # cache.ts など複数 feature 共有のサーバ処理 │ └── services/ # 複数 feature で使うビジネスロジック ├── stores/ # 共通ストア(error_message 等) ├── types/ # 共通型定義 diff --git a/prisma/seed.ts b/prisma/seed.ts index bf1f7bea3..9574a68ce 100755 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -19,7 +19,7 @@ import { defineWorkBookFactory, } from './.fabbrica'; import PQueue from 'p-queue'; -import { hashPassword } from '../src/lib/server/password'; +import { hashPassword } from '../src/features/auth/server/password'; import { getTaskGrade } from '../src/lib/types/task'; import type { PlacementCreate } from '../src/features/workbooks/types/workbook_placement'; diff --git a/src/app.d.ts b/src/app.d.ts index 16eef8e48..ec0f9a009 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -7,7 +7,7 @@ declare global { namespace App { // interface Error {} interface Locals { - auth: import('$lib/server/auth').AuthRequest; + auth: import('$features/auth/server/auth').AuthRequest; user: { id: string; name: string; diff --git a/src/lib/server/auth.test.ts b/src/features/auth/server/auth.test.ts similarity index 97% rename from src/lib/server/auth.test.ts rename to src/features/auth/server/auth.test.ts index 63394b690..768775131 100644 --- a/src/lib/server/auth.test.ts +++ b/src/features/auth/server/auth.test.ts @@ -6,13 +6,13 @@ import { Roles } from '@prisma/client'; // dev = false so `secure: !dev` resolves to true (the production-relevant cookie flag) vi.mock('$app/environment', () => ({ dev: false })); -vi.mock('$lib/server/session', () => ({ +vi.mock('./session', () => ({ SESSION_COOKIE_NAME: 'auth_session', validateSession: vi.fn(), })); import { createAuthRequest } from './auth'; -import { validateSession, SESSION_COOKIE_NAME } from '$lib/server/session'; +import { validateSession, SESSION_COOKIE_NAME } from './session'; const mockValidateSession = validateSession as unknown as ReturnType; diff --git a/src/lib/server/auth.ts b/src/features/auth/server/auth.ts similarity index 98% rename from src/lib/server/auth.ts rename to src/features/auth/server/auth.ts index 34053f0f2..b2e84047b 100644 --- a/src/lib/server/auth.ts +++ b/src/features/auth/server/auth.ts @@ -6,7 +6,7 @@ import { validateSession, type SessionCookieData, type ValidatedSession, -} from '$lib/server/session'; +} from './session'; export type AuthRequest = { validate: () => Promise; diff --git a/src/lib/server/password.test.ts b/src/features/auth/server/password.test.ts similarity index 100% rename from src/lib/server/password.test.ts rename to src/features/auth/server/password.test.ts diff --git a/src/lib/server/password.ts b/src/features/auth/server/password.ts similarity index 100% rename from src/lib/server/password.ts rename to src/features/auth/server/password.ts diff --git a/src/lib/server/random.test.ts b/src/features/auth/server/random.test.ts similarity index 100% rename from src/lib/server/random.test.ts rename to src/features/auth/server/random.test.ts diff --git a/src/lib/server/random.ts b/src/features/auth/server/random.ts similarity index 100% rename from src/lib/server/random.ts rename to src/features/auth/server/random.ts diff --git a/src/lib/server/session.test.ts b/src/features/auth/server/session.test.ts similarity index 100% rename from src/lib/server/session.test.ts rename to src/features/auth/server/session.test.ts diff --git a/src/lib/server/session.ts b/src/features/auth/server/session.ts similarity index 98% rename from src/lib/server/session.ts rename to src/features/auth/server/session.ts index d84b315c1..00eeb952f 100644 --- a/src/lib/server/session.ts +++ b/src/features/auth/server/session.ts @@ -2,7 +2,7 @@ import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; import type { Roles } from '@prisma/client'; import client from '$lib/server/database'; -import { generateRandomString } from '$lib/server/random'; +import { generateRandomString } from './random'; // lucia v2 defaults: active 24h, idle +14d const ACTIVE_PERIOD_MS = 1000 * 60 * 60 * 24; diff --git a/src/features/auth/services/credentials.test.ts b/src/features/auth/services/credentials.test.ts index ee91fb346..35e909937 100644 --- a/src/features/auth/services/credentials.test.ts +++ b/src/features/auth/services/credentials.test.ts @@ -15,13 +15,13 @@ vi.mock('$lib/server/database', () => ({ }, })); -vi.mock('$lib/server/password', () => ({ +vi.mock('../server/password', () => ({ hashPassword: vi.fn(), verifyPassword: vi.fn(), })); import db from '$lib/server/database'; -import { hashPassword, verifyPassword } from '$lib/server/password'; +import { hashPassword, verifyPassword } from '../server/password'; import { registerUser, authenticateUser } from './credentials'; const mockDb = db as unknown as { diff --git a/src/features/auth/services/credentials.ts b/src/features/auth/services/credentials.ts index c2d0a78bd..4d1022e1c 100644 --- a/src/features/auth/services/credentials.ts +++ b/src/features/auth/services/credentials.ts @@ -1,8 +1,8 @@ import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; import client from '$lib/server/database'; -import { hashPassword, verifyPassword } from '$lib/server/password'; -import { generateRandomString } from '$lib/server/random'; +import { hashPassword, verifyPassword } from '../server/password'; +import { generateRandomString } from '../server/random'; const USER_ID_LENGTH = 15; // lucia v2 createUser default diff --git a/src/features/auth/services/session.test.ts b/src/features/auth/services/session_guards.test.ts similarity index 99% rename from src/features/auth/services/session.test.ts rename to src/features/auth/services/session_guards.test.ts index 7b754eeab..96bcbe23f 100644 --- a/src/features/auth/services/session.test.ts +++ b/src/features/auth/services/session_guards.test.ts @@ -16,7 +16,7 @@ afterEach(() => { vi.clearAllMocks(); }); -import { ensureSessionOrRedirect, getLoggedInUser } from './session'; +import { ensureSessionOrRedirect, getLoggedInUser } from './session_guards'; const createMockLocalsWithValidSession = (user = { id: 'test-user', name: 'Test User' }) => ({ diff --git a/src/features/auth/services/session.ts b/src/features/auth/services/session_guards.ts similarity index 100% rename from src/features/auth/services/session.ts rename to src/features/auth/services/session_guards.ts diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 1f33289f2..53f489df7 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -3,7 +3,7 @@ // https://tech-blog.rakus.co.jp/entry/20230209/sveltekit import type { Handle } from '@sveltejs/kit'; -import { createAuthRequest } from '$lib/server/auth'; +import { createAuthRequest } from '$features/auth/server/auth'; import * as userService from '$lib/services/users'; diff --git a/src/lib/components/TagListForEdit.svelte b/src/lib/components/TagListForEdit.svelte index 49f4dc972..53bc907e3 100644 --- a/src/lib/components/TagListForEdit.svelte +++ b/src/lib/components/TagListForEdit.svelte @@ -14,7 +14,6 @@ //import { ATCODER_BASE_CONTEST_URL } from '$lib/constants/urls'; import { newline } from '$lib/utils/newline'; - //import { tasks } from '../server/sample_data'; //gradeでソート済みのTaskのリストと、APIから取得したtasklistを表示する //xport let tasks: Task[]; diff --git a/src/lib/server/sample_data.ts b/src/lib/server/sample_data.ts deleted file mode 100644 index c098cef0f..000000000 --- a/src/lib/server/sample_data.ts +++ /dev/null @@ -1,48 +0,0 @@ -// TODO: Enable to fetch data from the database via API. -export const tasks = [ - { - contest_id: 'abc318', - task_id: 'abc318_a', - title: 'A - foo', - grade: 'Q7', - }, - { - contest_id: 'abc231', - task_id: 'abc231_a', - title: 'A - Water Pressure', - grade: 'Q10', - }, - { - contest_id: 'abc214', - task_id: 'abc214_a', - title: 'A - New Generation ABC', - grade: 'Q10', - }, - { - contest_id: 'abc202', - task_id: 'abc202_a', - title: 'A - Three Dice', - grade: 'Q9', - }, -]; - -export const answers = [ - { - task_id: 'abc231_a', - user_id: 'hogehoge', - submission_status: 'wa', - status_id: '2', - }, - { - task_id: 'abc214_a', - user_id: 'hogehoge', - submission_status: 'ac', - status_id: '3', - }, - { - task_id: 'abc202_a', - user_id: 'hogehoge', - submission_status: 'ns', - status_id: '1', - }, -]; diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts index 0e6f15285..2aa11dec6 100644 --- a/src/routes/(auth)/login/+page.server.ts +++ b/src/routes/(auth)/login/+page.server.ts @@ -2,7 +2,7 @@ // See src/lib/utils/auth_forms.ts for the current form handling approach. import { fail, redirect } from '@sveltejs/kit'; -import { createSession } from '$lib/server/session'; +import { createSession } from '$features/auth/server/session'; import { authenticateUser } from '$features/auth/services/credentials'; import { initializeAuthForm, validateAuthFormWithFallback } from '$lib/utils/auth_forms'; diff --git a/src/routes/(auth)/logout/+page.server.ts b/src/routes/(auth)/logout/+page.server.ts index d16f20e37..f7802255f 100644 --- a/src/routes/(auth)/logout/+page.server.ts +++ b/src/routes/(auth)/logout/+page.server.ts @@ -1,6 +1,6 @@ import { fail, redirect } from '@sveltejs/kit'; -import { invalidateSession } from '$lib/server/session'; +import { invalidateSession } from '$features/auth/server/session'; import { SEE_OTHER, UNAUTHORIZED } from '$lib/constants/http-response-status-codes'; import { HOME_PAGE } from '$lib/constants/navbar-links'; diff --git a/src/routes/(auth)/signup/+page.server.ts b/src/routes/(auth)/signup/+page.server.ts index 22e757149..95db3feea 100644 --- a/src/routes/(auth)/signup/+page.server.ts +++ b/src/routes/(auth)/signup/+page.server.ts @@ -2,7 +2,7 @@ // See src/lib/utils/auth_forms.ts for the current form handling approach. import { fail, redirect } from '@sveltejs/kit'; -import { createSession } from '$lib/server/session'; +import { createSession } from '$features/auth/server/session'; import { registerUser } from '$features/auth/services/credentials'; import { initializeAuthForm, validateAuthFormWithFallback } from '$lib/utils/auth_forms'; diff --git a/src/routes/problems/[slug]/+page.server.ts b/src/routes/problems/[slug]/+page.server.ts index 32e33e8a9..a529830ba 100644 --- a/src/routes/problems/[slug]/+page.server.ts +++ b/src/routes/problems/[slug]/+page.server.ts @@ -2,7 +2,7 @@ import { fail, type Actions, redirect } from '@sveltejs/kit'; import * as crud from '$lib/services/task_results'; import { getButtons } from '$lib/services/submission_status'; -import { getLoggedInUser } from '$features/auth/services/session'; +import { getLoggedInUser } from '$features/auth/services/session_guards'; import { BAD_REQUEST, TEMPORARY_REDIRECT } from '$lib/constants/http-response-status-codes'; diff --git a/src/routes/users/[username]/+page.server.ts b/src/routes/users/[username]/+page.server.ts index 76daebfbc..0078522c9 100644 --- a/src/routes/users/[username]/+page.server.ts +++ b/src/routes/users/[username]/+page.server.ts @@ -2,12 +2,11 @@ import * as userService from '$lib/services/users'; import * as taskResultService from '$lib/services/task_results'; +import { getLoggedInUser } from '$features/auth/services/session_guards'; import type { Roles } from '$lib/types/user'; import type { TaskResult } from '$lib/types/task'; -import { getLoggedInUser } from '$features/auth/services/session'; - export async function load({ locals, params, url }) { const loggedInUser = await getLoggedInUser(locals, url); diff --git a/src/routes/users/edit/+page.server.ts b/src/routes/users/edit/+page.server.ts index 39f884086..b01be801d 100644 --- a/src/routes/users/edit/+page.server.ts +++ b/src/routes/users/edit/+page.server.ts @@ -6,8 +6,7 @@ import type { Roles } from '$lib/types/user'; import * as userService from '$lib/services/users'; import * as verificationService from '$features/account/services/atcoder_verification'; - -import { getLoggedInUser } from '$features/auth/services/session'; +import { getLoggedInUser } from '$features/auth/services/session_guards'; import { BAD_REQUEST, diff --git a/src/routes/workbooks/+page.server.ts b/src/routes/workbooks/+page.server.ts index 7bf04c196..12b90c522 100644 --- a/src/routes/workbooks/+page.server.ts +++ b/src/routes/workbooks/+page.server.ts @@ -1,9 +1,15 @@ import { error, redirect } from '@sveltejs/kit'; import * as taskCrud from '$lib/services/tasks'; -import { buildTaskIdsFromWorkbooks } from '$features/workbooks/utils/workbooks'; import * as taskResultsCrud from '$lib/services/task_results'; +import { getLoggedInUser } from '$features/auth/services/session_guards'; import * as workBooksCrud from '$features/workbooks/services/workbooks'; +import { + getWorkbooksByPlacement, + getWorkBooksCreatedByUsers, + getAvailableSolutionCategories, + getSolutionCategoryMapByWorkbookId, +} from '$features/workbooks/services/workbooks'; import { Roles } from '$lib/types/user'; import type { TaskGrade, TaskResult } from '$lib/types/task'; @@ -19,15 +25,8 @@ import { type SolutionCategory, } from '$features/workbooks/types/workbook_placement'; -import { - getWorkbooksByPlacement, - getWorkBooksCreatedByUsers, - getAvailableSolutionCategories, - getSolutionCategoryMapByWorkbookId, -} from '$features/workbooks/services/workbooks'; - import { isAdmin, canDelete } from '$lib/utils/authorship'; -import { getLoggedInUser } from '$features/auth/services/session'; +import { buildTaskIdsFromWorkbooks } from '$features/workbooks/utils/workbooks'; import { parseWorkBookTab, parseWorkBookGrade, diff --git a/src/routes/workbooks/[slug]/+page.server.ts b/src/routes/workbooks/[slug]/+page.server.ts index 336ed9684..c32ba7c07 100644 --- a/src/routes/workbooks/[slug]/+page.server.ts +++ b/src/routes/workbooks/[slug]/+page.server.ts @@ -4,15 +4,15 @@ import { zod4 } from 'sveltekit-superforms/adapters'; import { Roles } from '$lib/types/user'; +import { getLoggedInUser } from '$features/auth/services/session_guards'; import * as taskResultsCrud from '$lib/services/task_results'; -import { getWorkbookWithAuthor } from '$features/workbooks/services/workbooks'; import * as action from '$lib/actions/update_task_result'; +import { getWorkbookWithAuthor } from '$features/workbooks/services/workbooks'; import { getVoteGradeStatisticsForTaskIds } from '$features/votes/services/vote_statistics'; import { voteAbsoluteGrade as voteAbsoluteGradeAction } from '$features/votes/actions/vote_actions'; import { voteAbsoluteGradeSchema } from '$features/votes/zod/schema'; import { isAdmin, canRead } from '$lib/utils/authorship'; -import { getLoggedInUser } from '$features/auth/services/session'; import { parseWorkBookId, parseWorkBookUrlSlug } from '$features/workbooks/utils/workbook'; import { BAD_REQUEST, FORBIDDEN, NOT_FOUND } from '$lib/constants/http-response-status-codes'; diff --git a/src/routes/workbooks/create/+page.server.ts b/src/routes/workbooks/create/+page.server.ts index d645fffa3..81f6d57ba 100644 --- a/src/routes/workbooks/create/+page.server.ts +++ b/src/routes/workbooks/create/+page.server.ts @@ -2,14 +2,13 @@ import { error, fail, redirect } from '@sveltejs/kit'; import { superValidate } from 'sveltekit-superforms/server'; import { zod4 } from 'sveltekit-superforms/adapters'; -import { workBookSchema } from '$features/workbooks/zod/schema'; - import * as tasksCrud from '$lib/services/tasks'; +import { ensureSessionOrRedirect, getLoggedInUser } from '$features/auth/services/session_guards'; import * as workBooksCrud from '$features/workbooks/services/workbooks'; +import { workBookSchema } from '$features/workbooks/zod/schema'; import { Roles } from '$lib/types/user'; -import { ensureSessionOrRedirect, getLoggedInUser } from '$features/auth/services/session'; import { BAD_REQUEST, FORBIDDEN, diff --git a/src/routes/workbooks/edit/[slug]/+page.server.ts b/src/routes/workbooks/edit/[slug]/+page.server.ts index a14bb9875..79065edea 100644 --- a/src/routes/workbooks/edit/[slug]/+page.server.ts +++ b/src/routes/workbooks/edit/[slug]/+page.server.ts @@ -6,10 +6,10 @@ import { Roles } from '$lib/types/user'; import { workBookSchema } from '$features/workbooks/zod/schema'; import * as tasksCrud from '$lib/services/tasks'; +import { getLoggedInUser } from '$features/auth/services/session_guards'; import * as workBooksCrud from '$features/workbooks/services/workbooks'; import { canEdit, isAdmin } from '$lib/utils/authorship'; -import { getLoggedInUser } from '$features/auth/services/session'; import { parseWorkBookId, parseWorkBookUrlSlug } from '$features/workbooks/utils/workbook'; import {