Skip to content

Commit 803d242

Browse files
committed
some updates
1 parent d3d5d49 commit 803d242

File tree

8 files changed

+93
-185
lines changed

8 files changed

+93
-185
lines changed

docs/knowledges/data-models.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
# Data Models
22

3+
## Database Optimizations
4+
5+
### Better Auth Performance Indexes
6+
7+
Better Auth 推奨のパフォーマンス最適化インデックス(全て実装済み):
8+
9+
-`user.email` - unique constraint (自動インデックス)
10+
-`session.userId` - `session_userId_idx`
11+
-`session.token` - unique constraint (自動インデックス)
12+
-`account.userId` - `account_userId_idx`
13+
-`verification.identifier` - `verification_identifier_idx`
14+
15+
### Additional Indexes
16+
17+
CMS テーブル用の追加インデックス:
18+
19+
- `member.userId` - `member_userId_idx` (user linkage lookup)
20+
- `article.authorId` - `article_authorId_idx` (author's articles)
21+
- `article.published, publishedAt` - `article_published_publishedAt_idx` (published articles sorted by date)
22+
- `articleSlugRedirect.oldSlug` - `article_slug_redirect_oldSlug_idx` (redirect lookup)
23+
- `articleSlugRedirect.articleId` - `article_slug_redirect_articleId_idx` (article's redirects)
24+
- `projectMember.projectId, memberId` - `projectMember_pk` (project-member junction)
25+
- `viewLog.resourceType, resourceId` - `view_log_resourceType_resourceId_idx` (resource view lookups)
26+
- `viewLog.viewedAt` - `view_log_viewedAt_idx` (time-series analytics)
27+
28+
参考: [Better Auth Performance Guide](https://www.better-auth.com/docs/guides/optimizing-for-performance#database-optimizations)
29+
330
## Schema
431

532
```

docs/knowledges/project-context.md

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Project Context & Overview
22

3-
**Last Updated**: 2025-12-24
3+
**Last Updated**: 2025-12-26
44

55
## Project Identity
66

@@ -29,10 +29,10 @@ Component → *.remote.ts (DAL) → *.server.ts (DB)
2929
2. **DAL** (`*.remote.ts`): Auth guards via `getRequestEvent()`, exports `query`/`command`/`form`
3030
3. **DB** (`*.server.ts`): Pure database queries, no auth logic
3131

32-
**Verification (2025-12-24)**:
32+
**Verification (2025-12-26)**:
3333
- ✅ All `*.remote.ts` files contain auth guards (`requireUtCodeMember()`)
3434
- ✅ All `*.server.ts` files are pure DB/infrastructure (no auth)
35-
-`ownership.ts` (renamed from `ownership.server.ts`) is business logic, not DB layer
35+
-Trust-based model: No `ownership.ts` needed - all ut.code(); members have full admin access
3636
- ✅ Exception: `src/routes/(admin)/+layout.server.ts` (SvelteKit route-level auth guard)
3737

3838
### Remote Functions (`$app/server`)
@@ -108,8 +108,8 @@ bun tidy # Auto-check + fix (type + format + lint)
108108

109109
Current knowledge documents in `docs/knowledges/`:
110110

111-
1. **security.md** - Comprehensive security audit (2025-12-24)
112-
- Ownership-based access control model
111+
1. **security.md** - Comprehensive security audit (2025-12-26)
112+
- Trust-based security model (all ut.code(); members are mutually trusted)
113113
- All HIGH/MEDIUM security issues resolved
114114
- Remaining: LOW priority rate limiting and cache TTL
115115

@@ -157,13 +157,13 @@ c7ae749 meta: use sops for Docker build secrets
157157
## Current Priorities & TODOs
158158

159159
### Completed
160-
- ✅ Security audit and ownership controls
160+
- ✅ Security audit and trust-based access model
161161
- ✅ File upload validation (MIME whitelist, size limits, WebP compression)
162162
- ✅ LIKE wildcard escaping in search
163-
- ✅ Article authorId validation
164163
- ✅ Project role validation with picklist
165164
- ✅ Stats endpoint auth protection
166165
- ✅ All CRUD operations for members, articles, projects
166+
- ✅ Removed `ownership.ts` (2025-12-26) - trust-based model doesn't need resource-level checks
167167

168168
### Remaining (LOW Priority)
169169
- ⏳ Rate limiting on public endpoints (view counting, search)
@@ -176,16 +176,20 @@ c7ae749 meta: use sops for Docker build secrets
176176

177177
## Security Model
178178

179-
### Access Control
180-
| Role | Read Access | Write Access |
181-
| ------------------------ | ------------------------------------- | ------------------------------------------------------- |
182-
| Public (unauthenticated) | Published articles, members, projects | None |
183-
| ut.code(); members | All resources (including drafts) | Own resources only (ownership-based) |
179+
### Trust-Based Access Control
184180

185-
### Ownership Rules
186-
- **Articles**: Only author can edit/delete/publish/unpublish
187-
- **Members**: Only the member themselves can edit/delete their profile
188-
- **Projects**: Only project members (lead or regular) can edit/delete/manage members
181+
**Core Principle**: All ut.code(); members are mutually trusted.
182+
183+
| Role | Read Access | Write Access |
184+
| ------------------------ | ------------------------------------- | -------------------------------- |
185+
| Public (unauthenticated) | Published articles, members, projects | None |
186+
| ut.code(); members | All resources (including drafts) | All resources (full admin access)|
187+
188+
### Authorization Model
189+
- Single auth check: `requireUtCodeMember()` for all private endpoints
190+
- No resource-level ownership checks needed
191+
- All authenticated members can create, edit, delete any article, member profile, or project
192+
- Trust model prioritizes collaboration over granular access control
189193

190194
### Security Strengths
191195
- No SQL injection (Drizzle ORM with parameterized queries)

docs/knowledges/security.md

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
# Security Model
22

3-
## Assumptions
3+
## Trust Model
4+
5+
**Core Principle**: All ut.code(); members are mutually trusted.
6+
7+
This is an internal CMS for the ut.code(); organization. All authenticated members belong to the same organization and collaborate on shared content. Therefore:
48

5-
| Role | Read Access | Write Access |
6-
| ------------------------ | ------------------------------------- | ------------------------------------------------------- |
7-
| Public (unauthenticated) | Published articles, members, projects | None |
8-
| ut.code(); members | All resources (including drafts) | Own resources only (see Ownership Model below) |
9+
- **No ownership checks required**: Any ut.code(); member can edit any article, member profile, or project
10+
- **Authorization is binary**: `requireUtCodeMember()` is the only auth check needed
11+
- **Mutual trust assumption**: Members are expected to coordinate and communicate, not compete
12+
13+
This model prioritizes collaboration and simplicity over granular access control.
14+
15+
## Assumptions
916

10-
**Design Decision**: Ownership-based access control. Users can only modify resources they own or are members of.
17+
| Role | Read Access | Write Access |
18+
| ------------------------ | ------------------------------------- | -------------------------------- |
19+
| Public (unauthenticated) | Published articles, members, projects | None |
20+
| ut.code(); members | All resources (including drafts) | All resources (full admin access)|
1121

1222
## Authentication
1323

@@ -18,26 +28,20 @@
1828
- Mock user ID: `"mock"`, mock member ID: `"mock-member"`
1929
- Mock data returned from `getMemberByUserId`, `getUserPreference`, `setDefaultAuthor`
2030

21-
## Ownership Model
31+
## Authorization Model
2232

23-
Resource-level access control is enforced via `ownership.ts`:
33+
All authenticated ut.code(); members have full admin access. Authorization is enforced via:
2434

25-
### Articles
26-
- **Edit/Delete/Publish/Unpublish**: Only the article author can modify their own articles
27-
- Enforced by: `requireArticleOwnership(session, articleId)`
28-
- Creates: Any authenticated ut.code member can create articles (with their own member ID as author)
35+
- `requireUtCodeMember()` - Single auth check for all private endpoints
36+
- Returns session with `user.id` and `member.id`
37+
- No resource-level ownership checks needed
2938

30-
### Members
31-
- **Edit/Delete**: Only the member themselves can modify their own profile
32-
- Enforced by: `requireMemberOwnership(session, memberId)`
33-
- Creates: Any authenticated ut.code member can create member profiles
39+
### Resource Access
3440

35-
### Projects
36-
- **Edit/Delete**: Only project members (lead or regular members) can modify the project
37-
- **Add/Remove Members**: Only existing project members can add or remove other members
38-
- **Transfer Lead**: Only the current project lead can transfer leadership
39-
- Enforced by: `requireProjectOwnership(session, projectId)`
40-
- Creates: Any authenticated ut.code member can create projects (they become the lead)
41+
All authenticated members can:
42+
- **Articles**: Create, edit, delete, publish/unpublish any article
43+
- **Members**: Create, edit, delete any member profile
44+
- **Projects**: Create, edit, delete, manage members of any project
4145

4246
## Endpoint Classification
4347

@@ -72,7 +76,6 @@ Resource-level access control is enforced via `ownership.ts`:
7276
- **S3 Key Validation**: Regex prevents path traversal on delete (`S3KeySchema`)
7377
- **Session Management**: Better Auth handles session security
7478
- **CSRF**: SvelteKit Remote Functions use POST with same-origin enforcement
75-
- **AuthorId Validation**: Article `authorId` must be null or match authenticated user's member ID
7679

7780
## Known Issues
7881

@@ -98,19 +101,16 @@ Now uses `v.picklist(["lead", "member"])` validation.
98101
~~User input with `%`, `_`, or `\` characters not escaped in LIKE patterns.~~
99102
Now uses `escapeLikePattern()` utility in all search functions and redirect lookups.
100103

101-
### ~~MEDIUM: Article authorId not validated~~ FIXED
104+
### INFO: No resource-level access control (by design)
102105

103-
~~Users could set `authorId` to any member ID when creating/editing articles.~~
104-
Now validates that `authorId` is either null or matches authenticated user's member ID via `validateAuthorId()` helper.
106+
This is **not a security issue** - it's the intended trust model.
105107

106-
### ~~HIGH: No ownership verification on edits~~ FIXED
108+
All authenticated ut.code(); members can edit/delete any resource (articles, members, projects). This is intentional because:
109+
- All members belong to the same organization
110+
- Collaboration and flexibility are prioritized over access restrictions
111+
- Members are expected to coordinate via communication, not technical barriers
107112

108-
~~Any authenticated member could edit/delete any article, member profile, or project.~~
109-
Now enforces ownership checks:
110-
- Articles: Only author can edit/delete/publish/unpublish
111-
- Members: Only the member themselves can edit/delete their profile
112-
- Projects: Only project members can edit/delete/manage members
113-
Implementation: `ownership.ts` with `requireArticleOwnership()`, `requireMemberOwnership()`, `requireProjectOwnership()`
113+
**Implementation**: No `ownership.ts` module needed - `requireUtCodeMember()` is the only authorization check.
114114

115115
### LOW: No rate limiting
116116

@@ -128,13 +128,11 @@ GitHub org membership cached 24h. Removed members retain access.
128128
| Priority | Issue | Fix | Status |
129129
| ---------- | ------------------ | ------------------------------------------------------- | ------- |
130130
| ~~HIGH~~ | stats endpoint | Add `requireUtCodeMember()` or filter to published only | ✅ DONE |
131-
| ~~HIGH~~ | ownership controls | Add ownership verification for edit/delete operations | ✅ DONE |
132131
| ~~MEDIUM~~ | File upload | Add MIME whitelist, folder allowlist, WebP compression | ✅ DONE |
133132
| ~~MEDIUM~~ | Project role | Use `v.picklist(["lead", "member"])` | ✅ DONE |
134-
| ~~MEDIUM~~ | Article authId | Validate authorId matches user's member ID | ✅ DONE |
135133
| LOW | Rate limiting | Add to public endpoints | TODO |
136134
| LOW | Cache TTL | Reduce to 4h or add invalidation | TODO |
137135

138136
## Audit Date
139137

140-
2024-12-15 (Updated: 2025-12-24 - Added ownership verification model)
138+
2024-12-15 (Updated: 2025-12-26 - Updated to trust-based security model)

src/lib/data/private/articles.remote.ts

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { error } from "@sveltejs/kit";
21
import * as v from "valibot";
32
import { command, query } from "$app/server";
4-
import { validateAuthorId } from "$lib/server/database/article-validation";
53
import {
64
createArticle,
75
deleteArticle,
@@ -12,8 +10,6 @@ import {
1210
updateArticle,
1311
} from "$lib/server/database/articles.server";
1412
import { requireUtCodeMember } from "$lib/server/database/auth.server";
15-
import { getMemberByUserId } from "$lib/server/database/members.server";
16-
import { requireArticleOwnership } from "$lib/server/database/ownership";
1713
import { purgeCache } from "$lib/server/services/cloudflare/cache.server";
1814
import { DB_LARGE_LIMIT } from "$lib/shared/constants";
1915

@@ -38,13 +34,7 @@ export const saveArticle = command(
3834
publishedAt: v.nullable(v.date()),
3935
}),
4036
async (data) => {
41-
const session = await requireUtCodeMember();
42-
43-
// Validate authorId matches authenticated user's member ID
44-
const currentMember = await getMemberByUserId(session.user.id);
45-
if (!validateAuthorId(data.authorId, currentMember?.id ?? null)) {
46-
throw error(403, "Cannot set authorId to another user");
47-
}
37+
await requireUtCodeMember();
4838

4939
const result = await createArticle(data);
5040
purgeCache().catch(console.error);
@@ -67,18 +57,7 @@ export const editArticle = command(
6757
createRedirect: v.optional(v.boolean()),
6858
}),
6959
async ({ id, data, createRedirect }) => {
70-
const session = await requireUtCodeMember();
71-
72-
// Check ownership: only the author can edit the article
73-
await requireArticleOwnership(session, id);
74-
75-
// Validate authorId matches authenticated user's member ID (only if provided)
76-
if (data.authorId !== undefined) {
77-
const currentMember = await getMemberByUserId(session.user.id);
78-
if (!validateAuthorId(data.authorId, currentMember?.id ?? null)) {
79-
throw error(403, "Cannot set authorId to another user");
80-
}
81-
}
60+
await requireUtCodeMember();
8261

8362
// Create redirect if slug is changing and createRedirect is true
8463
if (data.slug !== undefined && createRedirect) {
@@ -100,31 +79,22 @@ export const editArticle = command(
10079
);
10180

10281
export const removeArticle = command(v.string(), async (id) => {
103-
const session = await requireUtCodeMember();
104-
105-
// Check ownership: only the author can delete the article
106-
await requireArticleOwnership(session, id);
82+
await requireUtCodeMember();
10783

10884
await deleteArticle(id);
10985
purgeCache().catch(console.error);
11086
});
11187

11288
export const publish = command(v.string(), async (id) => {
113-
const session = await requireUtCodeMember();
114-
115-
// Check ownership: only the author can publish the article
116-
await requireArticleOwnership(session, id);
89+
await requireUtCodeMember();
11790

11891
const result = await publishArticle(id);
11992
purgeCache().catch(console.error);
12093
return result;
12194
});
12295

12396
export const unpublish = command(v.string(), async (id) => {
124-
const session = await requireUtCodeMember();
125-
126-
// Check ownership: only the author can unpublish the article
127-
await requireArticleOwnership(session, id);
97+
await requireUtCodeMember();
12898

12999
const result = await unpublishArticle(id);
130100
purgeCache().catch(console.error);

src/lib/data/private/members.remote.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
listMembers,
99
updateMember,
1010
} from "$lib/server/database/members.server";
11-
import { requireMemberOwnership } from "$lib/server/database/ownership";
1211
import { purgeCache } from "$lib/server/services/cloudflare/cache.server";
1312
import { DB_MEMBERS_LIMIT } from "$lib/shared/constants";
1413

@@ -50,10 +49,7 @@ export const editMember = command(
5049
}),
5150
}),
5251
async ({ id, data }) => {
53-
const session = await requireUtCodeMember();
54-
55-
// Check ownership: only the member themselves can edit their profile
56-
await requireMemberOwnership(session, id);
52+
await requireUtCodeMember();
5753

5854
const result = await updateMember(id, data);
5955
purgeCache().catch(console.error);
@@ -62,10 +58,7 @@ export const editMember = command(
6258
);
6359

6460
export const removeMember = command(v.string(), async (id) => {
65-
const session = await requireUtCodeMember();
66-
67-
// Check ownership: only the member themselves can delete their profile
68-
await requireMemberOwnership(session, id);
61+
await requireUtCodeMember();
6962

7063
await deleteMember(id);
7164
purgeCache().catch(console.error);

0 commit comments

Comments
 (0)