@gencow/core 0.1.18 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crud.d.ts +18 -0
- package/dist/crud.js +231 -50
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/rls-db.d.ts +3 -5
- package/dist/rls-db.js +3 -5
- package/dist/rls.d.ts +44 -1
- package/dist/rls.js +62 -2
- package/dist/server.d.ts +1 -0
- package/dist/storage.d.ts +29 -2
- package/dist/storage.js +404 -15
- package/dist/v.js +5 -1
- package/package.json +42 -39
- package/src/__tests__/crud-owner-rls.test.ts +380 -0
- package/src/__tests__/fixtures/basic/auth.ts +32 -0
- package/src/__tests__/fixtures/basic/drizzle.config.ts +15 -0
- package/src/__tests__/fixtures/basic/index.ts +6 -0
- package/src/__tests__/fixtures/basic/migrations/0000_faithful_silver_sable.sql +66 -0
- package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +438 -0
- package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +13 -0
- package/src/__tests__/fixtures/basic/schema.ts +35 -0
- package/src/__tests__/fixtures/basic/tasks.ts +15 -0
- package/src/__tests__/fixtures/common/auth-schema.ts +63 -0
- package/src/__tests__/helpers/pglite-migrations.ts +35 -0
- package/src/__tests__/helpers/pglite-rls-session.ts +54 -0
- package/src/__tests__/helpers/seed-like-fill.ts +196 -0
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +53 -0
- package/src/__tests__/image-optimization.test.ts +652 -0
- package/src/__tests__/rls-crud-basic.test.ts +431 -0
- package/src/__tests__/storage.test.ts +113 -0
- package/src/__tests__/tsconfig.json +8 -0
- package/src/__tests__/validator.test.ts +35 -0
- package/src/crud.ts +270 -47
- package/src/index.ts +3 -2
- package/src/rls-db.ts +3 -5
- package/src/rls.ts +87 -3
- package/src/server.ts +1 -0
- package/src/storage.ts +481 -15
- package/src/v.ts +5 -1
- package/dist/scoped-db.d.ts +0 -34
- package/dist/scoped-db.js +0 -364
- package/dist/table.d.ts +0 -67
- package/dist/table.js +0 -98
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* packages/core/src/__tests__/crud-owner-rls.test.ts
|
|
3
|
+
*
|
|
4
|
+
* ownerRls + crud() 통합 테스트 — 2-Layer 방어 Layer 1 검증
|
|
5
|
+
*
|
|
6
|
+
* 검증 항목:
|
|
7
|
+
* 1. 격리: 타 사용자 데이터 접근 차단 (list, get, update, remove)
|
|
8
|
+
* 2. 자동주입: create 시 userId 강제 설정 (사용자 입력 무시)
|
|
9
|
+
* 3. read: "public": SELECT 필터 생략 + CUD 소유자 강제
|
|
10
|
+
* 4. 하위호환: ownerRls 미적용 테이블은 기존 동작 100% 유지
|
|
11
|
+
*
|
|
12
|
+
* Run: bun test packages/core/src/__tests__/crud-owner-rls.test.ts
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect } from "bun:test";
|
|
16
|
+
import { pgTable, serial, text, timestamp, pgPolicy } from "drizzle-orm/pg-core";
|
|
17
|
+
import { sql } from "drizzle-orm";
|
|
18
|
+
import { crud } from "../crud";
|
|
19
|
+
import { ownerRls, getOwnerRlsMeta, registerOwnerRls } from "../rls";
|
|
20
|
+
import { getQueryDef, getRegisteredMutations } from "../reactive";
|
|
21
|
+
|
|
22
|
+
// ─── 테스트 테이블 정의 ────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
// ownerRls 적용 테이블
|
|
25
|
+
const rlsTasks = pgTable("rls_tasks", {
|
|
26
|
+
id: serial("id").primaryKey(),
|
|
27
|
+
title: text("title").notNull(),
|
|
28
|
+
userId: text("user_id").notNull(),
|
|
29
|
+
createdAt: timestamp("created_at").defaultNow(),
|
|
30
|
+
updatedAt: timestamp("updated_at").defaultNow(),
|
|
31
|
+
}, (t) => ownerRls(t.userId));
|
|
32
|
+
|
|
33
|
+
// ownerRls(read: "public") 테이블
|
|
34
|
+
const rlsPosts = pgTable("rls_posts", {
|
|
35
|
+
id: serial("id").primaryKey(),
|
|
36
|
+
content: text("content").notNull(),
|
|
37
|
+
userId: text("user_id").notNull(),
|
|
38
|
+
createdAt: timestamp("created_at").defaultNow(),
|
|
39
|
+
}, (t) => ownerRls(t.userId, { read: "public" }));
|
|
40
|
+
|
|
41
|
+
// ownerRls 미적용 테이블 (하위호환 검증)
|
|
42
|
+
const plainItems = pgTable("plain_items", {
|
|
43
|
+
id: serial("id").primaryKey(),
|
|
44
|
+
name: text("name"),
|
|
45
|
+
userId: text("user_id"),
|
|
46
|
+
createdAt: timestamp("created_at").defaultNow(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ─── Mock 유틸 ─────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
function createMockCtx(userId: string, mockData: any[] = []) {
|
|
52
|
+
const dataChain: any = {
|
|
53
|
+
from: () => dataChain,
|
|
54
|
+
where: () => dataChain,
|
|
55
|
+
orderBy: () => dataChain,
|
|
56
|
+
limit: (n?: number) => n === 1
|
|
57
|
+
? Promise.resolve(mockData.slice(0, 1)) // get handler: .limit(1) → 단일 행 반환
|
|
58
|
+
: dataChain, // list handler: .limit(n).offset(m)
|
|
59
|
+
offset: () => Promise.resolve(mockData),
|
|
60
|
+
};
|
|
61
|
+
const countChain = {
|
|
62
|
+
from: () => countChain,
|
|
63
|
+
where: () => Promise.resolve([{ count: mockData.length }]),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
let capturedValues: any = null;
|
|
67
|
+
let capturedWhereArg: any = null;
|
|
68
|
+
const emitted: { key: string; data: any }[] = [];
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
ctx: {
|
|
72
|
+
auth: {
|
|
73
|
+
requireAuth: () => ({ id: userId, email: `${userId}@test.com` }),
|
|
74
|
+
getUserIdentity: () => ({ id: userId }),
|
|
75
|
+
getSession: () => null,
|
|
76
|
+
},
|
|
77
|
+
db: {
|
|
78
|
+
select: (selectArg?: any) => {
|
|
79
|
+
if (selectArg && typeof selectArg === "object" && "count" in selectArg) {
|
|
80
|
+
return countChain;
|
|
81
|
+
}
|
|
82
|
+
return dataChain;
|
|
83
|
+
},
|
|
84
|
+
insert: () => ({
|
|
85
|
+
values: (v: any) => {
|
|
86
|
+
capturedValues = v;
|
|
87
|
+
return { returning: () => Promise.resolve([{ ...v, id: 1 }]) };
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
update: () => ({
|
|
91
|
+
set: (data: any) => ({
|
|
92
|
+
where: (w: any) => {
|
|
93
|
+
capturedWhereArg = w;
|
|
94
|
+
return { returning: () => Promise.resolve([{ ...data, id: 1 }]) };
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
}),
|
|
98
|
+
delete: () => ({
|
|
99
|
+
where: (w: any) => {
|
|
100
|
+
capturedWhereArg = w;
|
|
101
|
+
return Promise.resolve();
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
},
|
|
105
|
+
realtime: {
|
|
106
|
+
emit: (key: string, data: any) => emitted.push({ key, data }),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
getCapturedValues: () => capturedValues,
|
|
110
|
+
getCapturedWhere: () => capturedWhereArg,
|
|
111
|
+
emitted,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function createUnauthCtx(mockData: any[] = []) {
|
|
116
|
+
const dataChain = {
|
|
117
|
+
from: () => dataChain,
|
|
118
|
+
where: () => dataChain,
|
|
119
|
+
orderBy: () => dataChain,
|
|
120
|
+
limit: () => dataChain,
|
|
121
|
+
offset: () => Promise.resolve(mockData),
|
|
122
|
+
};
|
|
123
|
+
const countChain = {
|
|
124
|
+
from: () => countChain,
|
|
125
|
+
where: () => Promise.resolve([{ count: mockData.length }]),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
auth: {
|
|
130
|
+
requireAuth: () => { throw new Error("Unauthorized"); },
|
|
131
|
+
getUserIdentity: () => null,
|
|
132
|
+
getSession: () => null,
|
|
133
|
+
},
|
|
134
|
+
db: {
|
|
135
|
+
select: (selectArg?: any) => {
|
|
136
|
+
if (selectArg && typeof selectArg === "object" && "count" in selectArg) {
|
|
137
|
+
return countChain;
|
|
138
|
+
}
|
|
139
|
+
return dataChain;
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
realtime: {
|
|
143
|
+
emit: () => {},
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
150
|
+
// Phase 1 검증: ownerRls() 메타데이터
|
|
151
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
152
|
+
|
|
153
|
+
describe("ownerRls() — 메타데이터 레지스트리", () => {
|
|
154
|
+
it("ownerRls 적용 테이블에서 메타데이터를 감지한다", () => {
|
|
155
|
+
// ownerRls()가 호출된 rlsTasks 테이블에서 WeakMap으로 건 못 찾더라도
|
|
156
|
+
// crud()가 getTableConfig().policies로 fallback 감지
|
|
157
|
+
// 여기서는 registerOwnerRls를 직접 호출하여 테스트
|
|
158
|
+
registerOwnerRls(rlsTasks, { columnName: "user_id", readPublic: false });
|
|
159
|
+
|
|
160
|
+
const meta = getOwnerRlsMeta(rlsTasks);
|
|
161
|
+
expect(meta).toBeDefined();
|
|
162
|
+
expect(meta!.columnName).toBe("user_id");
|
|
163
|
+
expect(meta!.readPublic).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("read: public 옵션이 메타데이터에 반영된다", () => {
|
|
167
|
+
registerOwnerRls(rlsPosts, { columnName: "user_id", readPublic: true });
|
|
168
|
+
|
|
169
|
+
const meta = getOwnerRlsMeta(rlsPosts);
|
|
170
|
+
expect(meta).toBeDefined();
|
|
171
|
+
expect(meta!.readPublic).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("ownerRls 미적용 테이블은 undefined 반환", () => {
|
|
175
|
+
const meta = getOwnerRlsMeta(plainItems);
|
|
176
|
+
expect(meta).toBeUndefined();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
182
|
+
// Phase 2 검증: crud() ownerRls 감지 + 필터 주입
|
|
183
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
184
|
+
|
|
185
|
+
describe("crud() + ownerRls — 데이터 격리", () => {
|
|
186
|
+
// 초기화: crud 등록
|
|
187
|
+
crud(rlsTasks);
|
|
188
|
+
crud(rlsPosts);
|
|
189
|
+
crud(plainItems);
|
|
190
|
+
|
|
191
|
+
// ── list 격리 ──
|
|
192
|
+
|
|
193
|
+
it("list: ownerRls 테이블에서 requireAuth가 호출된다", async () => {
|
|
194
|
+
const listDef = getQueryDef("rls_tasks.list");
|
|
195
|
+
expect(listDef).toBeDefined();
|
|
196
|
+
|
|
197
|
+
const { ctx } = createMockCtx("user-A", []);
|
|
198
|
+
// 에러 없이 실행되면 requireAuth 호출 성공
|
|
199
|
+
const result = await listDef!.handler(ctx, {});
|
|
200
|
+
expect(result).toHaveProperty("data");
|
|
201
|
+
expect(result).toHaveProperty("total");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("list: ownerRls 테이블은 인증 없이 접근 불가", async () => {
|
|
205
|
+
const listDef = getQueryDef("rls_tasks.list");
|
|
206
|
+
const unauthCtx = createUnauthCtx();
|
|
207
|
+
|
|
208
|
+
await expect(listDef!.handler(unauthCtx, {})).rejects.toThrow("Unauthorized");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ── get 격리 ──
|
|
212
|
+
|
|
213
|
+
it("get: ownerRls 테이블에서 requireAuth가 호출된다", async () => {
|
|
214
|
+
const getDef = getQueryDef("rls_tasks.get");
|
|
215
|
+
expect(getDef).toBeDefined();
|
|
216
|
+
|
|
217
|
+
const { ctx } = createMockCtx("user-A", [{ id: 1, title: "test", userId: "user-A" }]);
|
|
218
|
+
const result = await getDef!.handler(ctx, { id: 1 });
|
|
219
|
+
// null 또는 데이터 반환 (mock이므로 체이닝 결과)
|
|
220
|
+
// 핵심: 에러 없이 실행되면 성공
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ── create 자동 주입 ──
|
|
224
|
+
|
|
225
|
+
it("create: ownerRls 테이블에서 userId가 인증 사용자로 강제 설정된다", async () => {
|
|
226
|
+
const mutations = getRegisteredMutations();
|
|
227
|
+
const createDef = mutations.find((m: any) => m.name === "rls_tasks.create");
|
|
228
|
+
expect(createDef).toBeDefined();
|
|
229
|
+
|
|
230
|
+
const { ctx, getCapturedValues } = createMockCtx("user-A");
|
|
231
|
+
await createDef!.handler(ctx, { title: "New Task" });
|
|
232
|
+
|
|
233
|
+
// userId가 인증된 사용자 ID로 강제 설정됨 (JS 프로퍼티명 사용)
|
|
234
|
+
const values = getCapturedValues();
|
|
235
|
+
expect(values).toBeDefined();
|
|
236
|
+
expect(values.userId).toBe("user-A");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("create: 사용자가 임의의 userId를 주입 시도해도 강제 덮어씀 (보안)", async () => {
|
|
240
|
+
const mutations = getRegisteredMutations();
|
|
241
|
+
const createDef = mutations.find((m: any) => m.name === "rls_tasks.create");
|
|
242
|
+
|
|
243
|
+
const { ctx, getCapturedValues } = createMockCtx("user-A");
|
|
244
|
+
// 해커가 user_id를 "hacker-id"로 조작 시도
|
|
245
|
+
await createDef!.handler(ctx, { title: "Spoofed", user_id: "hacker-id" });
|
|
246
|
+
|
|
247
|
+
const values = getCapturedValues();
|
|
248
|
+
// 인증된 사용자 ID로 강제 덮어씀 (JS 프로퍼티명)
|
|
249
|
+
expect(values.userId).toBe("user-A");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ── update 격리 ──
|
|
253
|
+
|
|
254
|
+
it("update: ownerRls 테이블에서 userId 변경 시도가 차단된다", async () => {
|
|
255
|
+
const mutations = getRegisteredMutations();
|
|
256
|
+
const updateDef = mutations.find((m: any) => m.name === "rls_tasks.update");
|
|
257
|
+
expect(updateDef).toBeDefined();
|
|
258
|
+
|
|
259
|
+
// update에서는 set()에 전달되는 데이터에서 user_id 키가 삭제되어야 함
|
|
260
|
+
let capturedSetData: any = null;
|
|
261
|
+
const mockCtx = {
|
|
262
|
+
auth: {
|
|
263
|
+
requireAuth: () => ({ id: "user-A", email: "a@test.com" }),
|
|
264
|
+
getUserIdentity: () => ({ id: "user-A" }),
|
|
265
|
+
},
|
|
266
|
+
db: {
|
|
267
|
+
select: (selectArg?: any) => {
|
|
268
|
+
if (selectArg && typeof selectArg === "object" && "count" in selectArg) {
|
|
269
|
+
return { from: () => ({ where: () => Promise.resolve([{ count: 0 }]) }) };
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
from: () => ({ where: () => ({ orderBy: () => Promise.resolve([]) }) }),
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
update: () => ({
|
|
276
|
+
set: (data: any) => {
|
|
277
|
+
capturedSetData = data;
|
|
278
|
+
return {
|
|
279
|
+
where: () => ({
|
|
280
|
+
returning: () => Promise.resolve([{ id: 1, title: "Updated" }]),
|
|
281
|
+
}),
|
|
282
|
+
};
|
|
283
|
+
},
|
|
284
|
+
}),
|
|
285
|
+
},
|
|
286
|
+
realtime: { emit: () => {} },
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
await updateDef!.handler(mockCtx, {
|
|
290
|
+
id: 1,
|
|
291
|
+
title: "Updated",
|
|
292
|
+
user_id: "hacker-id", // userId 변경 시도
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// user_id가 set 데이터에서 삭제되었는지 확인 (JS 프로퍼티명 + DB명 양쪽)
|
|
296
|
+
expect(capturedSetData).toBeDefined();
|
|
297
|
+
expect(capturedSetData).not.toHaveProperty("userId");
|
|
298
|
+
expect(capturedSetData).not.toHaveProperty("user_id");
|
|
299
|
+
expect(capturedSetData.title).toBe("Updated");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// ── remove 격리 ──
|
|
303
|
+
|
|
304
|
+
it("remove: ownerRls 테이블에서 requireAuth가 호출된다", async () => {
|
|
305
|
+
const mutations = getRegisteredMutations();
|
|
306
|
+
const removeDef = mutations.find((m: any) => m.name === "rls_tasks.remove");
|
|
307
|
+
expect(removeDef).toBeDefined();
|
|
308
|
+
|
|
309
|
+
const { ctx } = createMockCtx("user-A");
|
|
310
|
+
const result = await removeDef!.handler(ctx, { id: 1 });
|
|
311
|
+
expect(result).toEqual({ success: true });
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
317
|
+
// read: "public" 테스트
|
|
318
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
319
|
+
|
|
320
|
+
describe("crud() + ownerRls(read: 'public') — 공개 읽기", () => {
|
|
321
|
+
it("list: 인증 없이 접근 가능 (read: public이므로 auth skip)", async () => {
|
|
322
|
+
// rlsPosts는 ownerRls(userId, { read: "public" })
|
|
323
|
+
// readPublic: true → list에서 userId 필터 생략
|
|
324
|
+
const listDef = getQueryDef("rls_posts.list");
|
|
325
|
+
expect(listDef).toBeDefined();
|
|
326
|
+
|
|
327
|
+
// crud는 isPublic=false로 생성되었으므로 requireAuth는 호출됨
|
|
328
|
+
// 하지만 readPublic=true이므로 userId 필터는 추가되지 않음
|
|
329
|
+
const { ctx } = createMockCtx("user-A", [{ id: 1, content: "hello" }]);
|
|
330
|
+
const result = await listDef!.handler(ctx, {});
|
|
331
|
+
expect(result.data).toHaveLength(1);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("create: 인증 필수 + userId 강제 주입", async () => {
|
|
335
|
+
const mutations = getRegisteredMutations();
|
|
336
|
+
const createDef = mutations.find((m: any) => m.name === "rls_posts.create");
|
|
337
|
+
expect(createDef).toBeDefined();
|
|
338
|
+
|
|
339
|
+
const { ctx, getCapturedValues } = createMockCtx("user-B");
|
|
340
|
+
await createDef!.handler(ctx, { content: "Public post" });
|
|
341
|
+
|
|
342
|
+
const values = getCapturedValues();
|
|
343
|
+
expect(values.userId).toBe("user-B");
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
349
|
+
// 하위호환 테스트 — ownerRls 미적용 테이블
|
|
350
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
351
|
+
|
|
352
|
+
describe("crud() — ownerRls 미적용 하위호환", () => {
|
|
353
|
+
it("ownerRls 없는 테이블에서 기존 userId 자동 주입 동작 유지", async () => {
|
|
354
|
+
const mutations = getRegisteredMutations();
|
|
355
|
+
const createDef = mutations.find((m: any) => m.name === "plain_items.create");
|
|
356
|
+
expect(createDef).toBeDefined();
|
|
357
|
+
|
|
358
|
+
const { ctx, getCapturedValues } = createMockCtx("user-C");
|
|
359
|
+
await createDef!.handler(ctx, { name: "Widget" });
|
|
360
|
+
|
|
361
|
+
const values = getCapturedValues();
|
|
362
|
+
// ownerRls 미적용 → 기존 자동 주입 로직: userId 컬럼이 있고 인증됐으면 주입
|
|
363
|
+
expect(values.userId).toBe("user-C");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("ownerRls 없는 테이블의 list는 userId 필터 없이 전체 반환", async () => {
|
|
367
|
+
const listDef = getQueryDef("plain_items.list");
|
|
368
|
+
expect(listDef).toBeDefined();
|
|
369
|
+
|
|
370
|
+
const allData = [
|
|
371
|
+
{ id: 1, name: "A", userId: "user-A" },
|
|
372
|
+
{ id: 2, name: "B", userId: "user-B" },
|
|
373
|
+
];
|
|
374
|
+
const { ctx } = createMockCtx("user-A", allData);
|
|
375
|
+
const result = await listDef!.handler(ctx, {});
|
|
376
|
+
|
|
377
|
+
// ownerRls 미적용 → 모든 데이터 반환 (기존 동작)
|
|
378
|
+
expect(result.data).toHaveLength(2);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gencow/auth.ts
|
|
3
|
+
*
|
|
4
|
+
* Auth 설정 파일. 이 파일을 수정하여 인증 동작을 커스터마이즈할 수 있습니다.
|
|
5
|
+
* shadcn처럼 이 파일은 사용자가 소유합니다 — 자유롭게 수정하세요.
|
|
6
|
+
*
|
|
7
|
+
* @example Email Verification 활성화:
|
|
8
|
+
* 1. `bun add resend`
|
|
9
|
+
* 2. 아래 emailVerification 블록 주석 해제
|
|
10
|
+
* 3. `gencow env set RESEND_API_KEY re_xxxx`
|
|
11
|
+
*
|
|
12
|
+
* @see https://docs.gencow.com/auth
|
|
13
|
+
*/
|
|
14
|
+
import { defineAuth } from "../../../auth-config";
|
|
15
|
+
|
|
16
|
+
export default defineAuth({
|
|
17
|
+
// ── Email Verification (선택) ──────────────────────
|
|
18
|
+
// 아래 주석을 해제하면 가입 시 이메일 인증이 활성화됩니다.
|
|
19
|
+
//
|
|
20
|
+
// emailVerification: {
|
|
21
|
+
// sendVerificationEmail: async ({ user, url }) => {
|
|
22
|
+
// const { Resend } = await import("resend");
|
|
23
|
+
// const resend = new Resend(process.env.RESEND_API_KEY);
|
|
24
|
+
// await resend.emails.send({
|
|
25
|
+
// from: "noreply@yourapp.com",
|
|
26
|
+
// to: user.email,
|
|
27
|
+
// subject: "이메일 인증",
|
|
28
|
+
// html: `<a href="${url}">인증하기</a>`,
|
|
29
|
+
// });
|
|
30
|
+
// },
|
|
31
|
+
// },
|
|
32
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from "drizzle-kit";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
/** Config dir — schema/out paths must not depend on process.cwd (pnpm exec uses package root). */
|
|
6
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
dialect: "postgresql",
|
|
10
|
+
schema: [
|
|
11
|
+
resolve(here, "schema.ts"),
|
|
12
|
+
resolve(here, "../common/auth-schema.ts"),
|
|
13
|
+
],
|
|
14
|
+
out: resolve(here, "migrations"),
|
|
15
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
CREATE TABLE "tasks" (
|
|
2
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
3
|
+
"title" text NOT NULL,
|
|
4
|
+
"description" text,
|
|
5
|
+
"done" boolean DEFAULT false NOT NULL,
|
|
6
|
+
"user_id" text NOT NULL,
|
|
7
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
8
|
+
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
9
|
+
);
|
|
10
|
+
--> statement-breakpoint
|
|
11
|
+
ALTER TABLE "tasks" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
|
12
|
+
CREATE TABLE "user" (
|
|
13
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
14
|
+
"name" text NOT NULL,
|
|
15
|
+
"email" text NOT NULL,
|
|
16
|
+
"email_verified" boolean DEFAULT false NOT NULL,
|
|
17
|
+
"image" text,
|
|
18
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
19
|
+
"updated_at" timestamp DEFAULT now() NOT NULL,
|
|
20
|
+
CONSTRAINT "user_email_unique" UNIQUE("email")
|
|
21
|
+
);
|
|
22
|
+
--> statement-breakpoint
|
|
23
|
+
CREATE TABLE "account" (
|
|
24
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
25
|
+
"account_id" text NOT NULL,
|
|
26
|
+
"provider_id" text NOT NULL,
|
|
27
|
+
"user_id" text NOT NULL,
|
|
28
|
+
"access_token" text,
|
|
29
|
+
"refresh_token" text,
|
|
30
|
+
"id_token" text,
|
|
31
|
+
"access_token_expires_at" timestamp,
|
|
32
|
+
"refresh_token_expires_at" timestamp,
|
|
33
|
+
"scope" text,
|
|
34
|
+
"password" text,
|
|
35
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
36
|
+
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
37
|
+
);
|
|
38
|
+
--> statement-breakpoint
|
|
39
|
+
CREATE TABLE "session" (
|
|
40
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
41
|
+
"expires_at" timestamp NOT NULL,
|
|
42
|
+
"token" text NOT NULL,
|
|
43
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
44
|
+
"updated_at" timestamp DEFAULT now() NOT NULL,
|
|
45
|
+
"ip_address" text,
|
|
46
|
+
"user_agent" text,
|
|
47
|
+
"user_id" text NOT NULL,
|
|
48
|
+
CONSTRAINT "session_token_unique" UNIQUE("token")
|
|
49
|
+
);
|
|
50
|
+
--> statement-breakpoint
|
|
51
|
+
CREATE TABLE "verification" (
|
|
52
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
53
|
+
"identifier" text NOT NULL,
|
|
54
|
+
"value" text NOT NULL,
|
|
55
|
+
"expires_at" timestamp NOT NULL,
|
|
56
|
+
"created_at" timestamp DEFAULT now(),
|
|
57
|
+
"updated_at" timestamp DEFAULT now()
|
|
58
|
+
);
|
|
59
|
+
--> statement-breakpoint
|
|
60
|
+
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
61
|
+
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
62
|
+
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
63
|
+
CREATE POLICY "rls-select" ON "tasks" AS PERMISSIVE FOR SELECT TO public USING ("tasks"."user_id" = current_setting('app.current_user_id', true));--> statement-breakpoint
|
|
64
|
+
CREATE POLICY "rls-insert" ON "tasks" AS PERMISSIVE FOR INSERT TO public WITH CHECK ("tasks"."user_id" = current_setting('app.current_user_id', true));--> statement-breakpoint
|
|
65
|
+
CREATE POLICY "rls-update" ON "tasks" AS PERMISSIVE FOR UPDATE TO public USING ("tasks"."user_id" = current_setting('app.current_user_id', true)) WITH CHECK ("tasks"."user_id" = current_setting('app.current_user_id', true));--> statement-breakpoint
|
|
66
|
+
CREATE POLICY "rls-delete" ON "tasks" AS PERMISSIVE FOR DELETE TO public USING ("tasks"."user_id" = current_setting('app.current_user_id', true));
|