@fragno-dev/auth 0.0.14
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/LICENSE.md +16 -0
- package/README.md +16 -0
- package/dist/browser/client/react.d.ts +122 -0
- package/dist/browser/client/react.d.ts.map +1 -0
- package/dist/browser/client/react.js +11 -0
- package/dist/browser/client/react.js.map +1 -0
- package/dist/browser/client/solid.d.ts +122 -0
- package/dist/browser/client/solid.d.ts.map +1 -0
- package/dist/browser/client/solid.js +11 -0
- package/dist/browser/client/solid.js.map +1 -0
- package/dist/browser/client/svelte.d.ts +122 -0
- package/dist/browser/client/svelte.d.ts.map +1 -0
- package/dist/browser/client/svelte.js +11 -0
- package/dist/browser/client/svelte.js.map +1 -0
- package/dist/browser/client/vanilla.d.ts +122 -0
- package/dist/browser/client/vanilla.d.ts.map +1 -0
- package/dist/browser/client/vanilla.js +11 -0
- package/dist/browser/client/vanilla.js.map +1 -0
- package/dist/browser/client/vue.d.ts +122 -0
- package/dist/browser/client/vue.d.ts.map +1 -0
- package/dist/browser/client/vue.js +11 -0
- package/dist/browser/client/vue.js.map +1 -0
- package/dist/browser/index.d.ts +600 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +3 -0
- package/dist/browser/src-DNrh9CQq.js +184 -0
- package/dist/browser/src-DNrh9CQq.js.map +1 -0
- package/dist/node/index.d.ts +600 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +677 -0
- package/dist/node/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +97 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { defineFragment, defineRoute, defineRoutes } from "@fragno-dev/core";
|
|
2
|
+
import { createClientBuilder } from "@fragno-dev/core/client";
|
|
3
|
+
import { schema } from "@fragno-dev/db/schema";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import "@fragno-dev/db/cursor";
|
|
6
|
+
|
|
7
|
+
//#region src/schema.ts
|
|
8
|
+
const authSchema = schema("auth", (s) => s);
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/user/user-actions.ts
|
|
12
|
+
const userActionsRoutesFactory = defineRoutes().create(({ services, config }) => {
|
|
13
|
+
return [
|
|
14
|
+
defineRoute({
|
|
15
|
+
method: "PATCH",
|
|
16
|
+
path: "/users/:userId/role",
|
|
17
|
+
inputSchema: z.object({ role: z.enum(["user", "admin"]) }),
|
|
18
|
+
outputSchema: z.object({ success: z.boolean() }),
|
|
19
|
+
errorCodes: [
|
|
20
|
+
"invalid_input",
|
|
21
|
+
"session_invalid",
|
|
22
|
+
"permission_denied"
|
|
23
|
+
],
|
|
24
|
+
handler: () => {}
|
|
25
|
+
}),
|
|
26
|
+
defineRoute({
|
|
27
|
+
method: "POST",
|
|
28
|
+
path: "/sign-up",
|
|
29
|
+
inputSchema: z.object({
|
|
30
|
+
email: z.email(),
|
|
31
|
+
password: z.string().min(8).max(100)
|
|
32
|
+
}),
|
|
33
|
+
outputSchema: z.object({
|
|
34
|
+
sessionId: z.string(),
|
|
35
|
+
userId: z.string(),
|
|
36
|
+
email: z.string(),
|
|
37
|
+
role: z.enum(["user", "admin"])
|
|
38
|
+
}),
|
|
39
|
+
errorCodes: ["email_already_exists", "invalid_input"],
|
|
40
|
+
handler: () => {}
|
|
41
|
+
}),
|
|
42
|
+
defineRoute({
|
|
43
|
+
method: "POST",
|
|
44
|
+
path: "/sign-in",
|
|
45
|
+
inputSchema: z.object({
|
|
46
|
+
email: z.email(),
|
|
47
|
+
password: z.string().min(8).max(100)
|
|
48
|
+
}),
|
|
49
|
+
outputSchema: z.object({
|
|
50
|
+
sessionId: z.string(),
|
|
51
|
+
userId: z.string(),
|
|
52
|
+
email: z.string(),
|
|
53
|
+
role: z.enum(["user", "admin"])
|
|
54
|
+
}),
|
|
55
|
+
errorCodes: ["invalid_credentials"],
|
|
56
|
+
handler: () => {}
|
|
57
|
+
}),
|
|
58
|
+
defineRoute({
|
|
59
|
+
method: "POST",
|
|
60
|
+
path: "/change-password",
|
|
61
|
+
inputSchema: z.object({ newPassword: z.string().min(8).max(100) }),
|
|
62
|
+
outputSchema: z.object({ success: z.boolean() }),
|
|
63
|
+
errorCodes: ["session_invalid"],
|
|
64
|
+
handler: () => {}
|
|
65
|
+
})
|
|
66
|
+
];
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/session/session.ts
|
|
71
|
+
const sessionRoutesFactory = defineRoutes().create(({ services, config }) => {
|
|
72
|
+
return [defineRoute({
|
|
73
|
+
method: "POST",
|
|
74
|
+
path: "/sign-out",
|
|
75
|
+
inputSchema: z.object({ sessionId: z.string().optional() }).optional(),
|
|
76
|
+
outputSchema: z.object({ success: z.boolean() }),
|
|
77
|
+
errorCodes: ["session_not_found"],
|
|
78
|
+
handler: () => {}
|
|
79
|
+
}), defineRoute({
|
|
80
|
+
method: "GET",
|
|
81
|
+
path: "/me",
|
|
82
|
+
queryParameters: ["sessionId"],
|
|
83
|
+
outputSchema: z.object({
|
|
84
|
+
userId: z.string(),
|
|
85
|
+
email: z.string(),
|
|
86
|
+
role: z.enum(["user", "admin"])
|
|
87
|
+
}),
|
|
88
|
+
errorCodes: ["session_invalid"],
|
|
89
|
+
handler: () => {}
|
|
90
|
+
})];
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/user/user-overview.ts
|
|
95
|
+
const sortBySchema = z.enum(["email", "createdAt"]);
|
|
96
|
+
const userOverviewRoutesFactory = defineRoutes().create(({ services }) => {
|
|
97
|
+
return [defineRoute({
|
|
98
|
+
method: "GET",
|
|
99
|
+
path: "/users",
|
|
100
|
+
queryParameters: [
|
|
101
|
+
"search",
|
|
102
|
+
"sortBy",
|
|
103
|
+
"sortOrder",
|
|
104
|
+
"pageSize",
|
|
105
|
+
"cursor"
|
|
106
|
+
],
|
|
107
|
+
outputSchema: z.object({
|
|
108
|
+
users: z.array(z.object({
|
|
109
|
+
id: z.string(),
|
|
110
|
+
email: z.string(),
|
|
111
|
+
role: z.enum(["user", "admin"]),
|
|
112
|
+
createdAt: z.string()
|
|
113
|
+
})),
|
|
114
|
+
cursor: z.string().optional(),
|
|
115
|
+
hasNextPage: z.boolean(),
|
|
116
|
+
sortBy: sortBySchema
|
|
117
|
+
}),
|
|
118
|
+
errorCodes: ["invalid_input"],
|
|
119
|
+
handler: () => {}
|
|
120
|
+
})];
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/index.ts
|
|
125
|
+
const authFragmentDefinition = defineFragment("auth").extend((x) => x).providesBaseService(() => {}).build();
|
|
126
|
+
function createAuthFragment(config = {}, fragnoConfig) {
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
function createAuthFragmentClients(fragnoConfig) {
|
|
130
|
+
const config = { ...fragnoConfig };
|
|
131
|
+
const b = createClientBuilder(authFragmentDefinition, config, [
|
|
132
|
+
userActionsRoutesFactory,
|
|
133
|
+
sessionRoutesFactory,
|
|
134
|
+
userOverviewRoutesFactory
|
|
135
|
+
], {
|
|
136
|
+
type: "options",
|
|
137
|
+
options: { credentials: "include" }
|
|
138
|
+
});
|
|
139
|
+
const useMe = b.createHook("/me");
|
|
140
|
+
const useSignUp = b.createMutator("POST", "/sign-up");
|
|
141
|
+
const useSignIn = b.createMutator("POST", "/sign-in");
|
|
142
|
+
const useSignOut = b.createMutator("POST", "/sign-out", (invalidate) => {
|
|
143
|
+
invalidate("GET", "/me", {});
|
|
144
|
+
invalidate("GET", "/users", {});
|
|
145
|
+
});
|
|
146
|
+
const useUsers = b.createHook("/users");
|
|
147
|
+
const useUpdateUserRole = b.createMutator("PATCH", "/users/:userId/role", (invalidate) => {
|
|
148
|
+
invalidate("GET", "/users", {});
|
|
149
|
+
invalidate("GET", "/me", {});
|
|
150
|
+
});
|
|
151
|
+
const useChangePassword = b.createMutator("POST", "/change-password");
|
|
152
|
+
return {
|
|
153
|
+
useSignUp,
|
|
154
|
+
useSignIn,
|
|
155
|
+
useSignOut,
|
|
156
|
+
useMe,
|
|
157
|
+
useUsers,
|
|
158
|
+
useUpdateUserRole,
|
|
159
|
+
useChangePassword,
|
|
160
|
+
signIn: { email: async ({ email, password, rememberMe: _rememberMe }) => {
|
|
161
|
+
return useSignIn.mutateQuery({ body: {
|
|
162
|
+
email,
|
|
163
|
+
password
|
|
164
|
+
} });
|
|
165
|
+
} },
|
|
166
|
+
signUp: { email: async ({ email, password }) => {
|
|
167
|
+
return useSignUp.mutateQuery({ body: {
|
|
168
|
+
email,
|
|
169
|
+
password
|
|
170
|
+
} });
|
|
171
|
+
} },
|
|
172
|
+
signOut: (params) => {
|
|
173
|
+
return useSignOut.mutateQuery({ body: params?.sessionId ? { sessionId: params.sessionId } : {} });
|
|
174
|
+
},
|
|
175
|
+
me: async (params) => {
|
|
176
|
+
if (params?.sessionId) return useMe.query({ query: { sessionId: params.sessionId } });
|
|
177
|
+
return useMe.query();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
//#endregion
|
|
183
|
+
export { authFragmentDefinition, createAuthFragment, createAuthFragmentClients };
|
|
184
|
+
//# sourceMappingURL=src-DNrh9CQq.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src-DNrh9CQq.js","names":["schema","authSchema","s","defineRoute","defineRoutes","DatabaseServiceContext","authSchema","z","authFragmentDefinition","AuthServiceContext","createUserServices","createUser","this","email","passwordHash","role","serviceTx","mutate","uow","id","create","valueOf","build","getUserByEmail","retrieve","findFirst","b","whereIndex","eb","transformRetrieve","user","updateUserRole","userId","update","set","success","updateUserPassword","signUpWithSession","expiresAt","Date","setDate","getDate","retrieveResult","existingUser","ok","const","code","sessionId","updateUserRoleWithSession","now","join","j","sessionOwner","select","session","delete","check","changePasswordWithSession","userActionsRoutesFactory","services","config","method","path","inputSchema","object","enum","outputSchema","boolean","errorCodes","handler","password","string","min","max","newPassword","defineRoute","defineRoutes","DatabaseServiceContext","authSchema","z","buildSetCookieHeader","extractSessionId","CookieOptions","authFragmentDefinition","AuthServiceContext","createSessionServices","cookieOptions","services","buildSessionCookie","sessionId","createSession","this","userId","expiresAt","Date","setDate","getDate","serviceTx","mutate","uow","id","create","valueOf","build","validateSession","now","retrieve","findFirst","b","whereIndex","eb","join","j","sessionOwner","select","retrieveResult","session","delete","check","user","email","role","invalidateSession","getSession","headers","Headers","undefined","sessionRoutesFactory","config","method","path","inputSchema","object","string","optional","outputSchema","success","boolean","errorCodes","handler","queryParameters","enum","defineRoute","defineRoutes","DatabaseServiceContext","Cursor","authSchema","z","authFragmentDefinition","SortField","SortOrder","GetUsersParams","search","sortBy","sortOrder","pageSize","cursor","UserResult","id","email","role","createdAt","Date","AuthServiceContext","createUserOverviewServices","mapUser","user","String","getUsersWithCursor","this","params","effectiveSortBy","indexName","effectiveSortOrder","orderDirection","effectivePageSize","serviceTx","retrieve","uow","findWithCursor","b","query","whereIndex","eb","orderByIndex","after","transformRetrieve","result","users","items","map","hasNextPage","build","sortBySchema","enum","userOverviewRoutesFactory","create","services","method","path","queryParameters","outputSchema","object","array","string","optional","boolean","errorCodes","handler","defineFragment","createClientBuilder","FragnoPublicClientConfig","userActionsRoutesFactory","sessionRoutesFactory","userOverviewRoutesFactory","GetUsersParams","UserResult","SortField","SortOrder","CookieOptions","AuthConfig","sendEmail","params","to","subject","body","Promise","cookieOptions","authFragmentDefinition","extend","x","providesBaseService","build","AuthFragment","createAuthFragment","config","fragnoConfig","FragnoPublicConfigWithDatabase","createAuthFragmentClients","b","type","options","credentials","useMe","createHook","useSignUp","createMutator","useSignIn","useSignOut","invalidate","useUsers","useUpdateUserRole","useChangePassword","signIn","email","password","rememberMe","_rememberMe","mutateQuery","signUp","signOut","sessionId","me","query","FragnoRouteConfig","Role"],"sources":["../../src/schema.ts","../../src/user/user-actions.ts","../../src/session/session.ts","../../src/user/user-overview.ts","../../src/index.ts"],"sourcesContent":["import { column, idColumn, referenceColumn, schema } from \"@fragno-dev/db/schema\";\n\nexport const authSchema = schema(\"auth\", (s) => {\n return s\n .addTable(\"user\", (t) => {\n return t\n .addColumn(\"id\", idColumn())\n .addColumn(\"email\", column(\"string\"))\n .addColumn(\"passwordHash\", column(\"string\"))\n .addColumn(\"role\", column(\"string\").defaultTo(\"user\"))\n .addColumn(\n \"createdAt\",\n column(\"timestamp\").defaultTo((b) => b.now()),\n )\n .createIndex(\"idx_user_email\", [\"email\"])\n .createIndex(\"idx_user_id\", [\"id\"], { unique: true });\n })\n .addTable(\"session\", (t) => {\n return t\n .addColumn(\"id\", idColumn())\n .addColumn(\"userId\", referenceColumn())\n .addColumn(\"expiresAt\", column(\"timestamp\"))\n .addColumn(\n \"createdAt\",\n column(\"timestamp\").defaultTo((b) => b.now()),\n )\n .createIndex(\"idx_session_user\", [\"userId\"]);\n })\n .addReference(\"sessionOwner\", {\n from: {\n table: \"session\",\n column: \"userId\",\n },\n to: {\n table: \"user\",\n column: \"id\",\n },\n type: \"one\",\n })\n .alterTable(\"user\", (t) => {\n return t.createIndex(\"idx_user_createdAt\", [\"createdAt\"]);\n });\n});\n","import { defineRoute, defineRoutes } from \"@fragno-dev/core\";\nimport type { DatabaseServiceContext } from \"@fragno-dev/db\";\nimport { authSchema } from \"../schema\";\nimport { z } from \"zod\";\nimport { hashPassword, verifyPassword } from \"./password\";\nimport { buildSetCookieHeader, extractSessionId } from \"../utils/cookie\";\nimport type { authFragmentDefinition } from \"..\";\n\ntype AuthServiceContext = DatabaseServiceContext<{}>;\n\nexport function createUserServices() {\n return {\n createUser: function (\n this: AuthServiceContext,\n email: string,\n passwordHash: string,\n role: \"user\" | \"admin\" = \"user\",\n ) {\n return this.serviceTx(authSchema)\n .mutate(({ uow }) => {\n const id = uow.create(\"user\", {\n email,\n passwordHash,\n role,\n });\n return {\n id: id.valueOf(),\n email,\n role,\n };\n })\n .build();\n },\n getUserByEmail: function (this: AuthServiceContext, email: string) {\n return this.serviceTx(authSchema)\n .retrieve((uow) =>\n uow.findFirst(\"user\", (b) =>\n b.whereIndex(\"idx_user_email\", (eb) => eb(\"email\", \"=\", email)),\n ),\n )\n .transformRetrieve(([user]) =>\n user\n ? {\n id: user.id.valueOf(),\n email: user.email,\n passwordHash: user.passwordHash,\n role: user.role as \"user\" | \"admin\",\n }\n : null,\n )\n .build();\n },\n updateUserRole: function (this: AuthServiceContext, userId: string, role: \"user\" | \"admin\") {\n return this.serviceTx(authSchema)\n .mutate(({ uow }) => {\n uow.update(\"user\", userId, (b) => b.set({ role }));\n return { success: true };\n })\n .build();\n },\n updateUserPassword: function (this: AuthServiceContext, userId: string, passwordHash: string) {\n return this.serviceTx(authSchema)\n .mutate(({ uow }) => {\n uow.update(\"user\", userId, (b) => b.set({ passwordHash }));\n return { success: true };\n })\n .build();\n },\n signUpWithSession: function (this: AuthServiceContext, email: string, passwordHash: string) {\n const expiresAt = new Date();\n expiresAt.setDate(expiresAt.getDate() + 30); // 30 days from now\n\n return this.serviceTx(authSchema)\n .retrieve((uow) =>\n uow.findFirst(\"user\", (b) =>\n b.whereIndex(\"idx_user_email\", (eb) => eb(\"email\", \"=\", email)),\n ),\n )\n .mutate(({ uow, retrieveResult: [existingUser] }) => {\n if (existingUser) {\n return { ok: false as const, code: \"email_already_exists\" as const };\n }\n\n const userId = uow.create(\"user\", {\n email,\n passwordHash,\n role: \"user\",\n });\n\n const sessionId = uow.create(\"session\", {\n userId,\n expiresAt,\n });\n\n return {\n ok: true as const,\n sessionId: sessionId.valueOf(),\n userId: userId.valueOf(),\n email,\n role: \"user\" as const,\n };\n })\n .build();\n },\n updateUserRoleWithSession: function (\n this: AuthServiceContext,\n sessionId: string,\n userId: string,\n role: \"user\" | \"admin\",\n ) {\n const now = new Date();\n return this.serviceTx(authSchema)\n .retrieve((uow) =>\n uow.findFirst(\"session\", (b) =>\n b\n .whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", sessionId))\n .join((j) => j.sessionOwner((b) => b.select([\"id\", \"email\", \"role\"]))),\n ),\n )\n .mutate(({ uow, retrieveResult: [session] }) => {\n if (!session || !session.sessionOwner) {\n return { ok: false as const, code: \"session_invalid\" as const };\n }\n\n if (session.expiresAt < now) {\n uow.delete(\"session\", session.id, (b) => b.check());\n return { ok: false as const, code: \"session_invalid\" as const };\n }\n\n if (session.sessionOwner.role !== \"admin\") {\n return { ok: false as const, code: \"permission_denied\" as const };\n }\n\n uow.update(\"user\", userId, (b) => b.set({ role }));\n return { ok: true as const };\n })\n .build();\n },\n changePasswordWithSession: function (\n this: AuthServiceContext,\n sessionId: string,\n passwordHash: string,\n ) {\n const now = new Date();\n return this.serviceTx(authSchema)\n .retrieve((uow) =>\n uow.findFirst(\"session\", (b) =>\n b\n .whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", sessionId))\n .join((j) => j.sessionOwner((b) => b.select([\"id\", \"email\", \"role\"]))),\n ),\n )\n .mutate(({ uow, retrieveResult: [session] }) => {\n if (!session || !session.sessionOwner) {\n return { ok: false as const, code: \"session_invalid\" as const };\n }\n\n if (session.expiresAt < now) {\n uow.delete(\"session\", session.id, (b) => b.check());\n return { ok: false as const, code: \"session_invalid\" as const };\n }\n\n uow.update(\"user\", session.sessionOwner.id, (b) => b.set({ passwordHash }).check());\n return { ok: true as const };\n })\n .build();\n },\n };\n}\n\nexport const userActionsRoutesFactory = defineRoutes<typeof authFragmentDefinition>().create(\n ({ services, config }) => {\n return [\n defineRoute({\n method: \"PATCH\",\n path: \"/users/:userId/role\",\n inputSchema: z.object({\n role: z.enum([\"user\", \"admin\"]),\n }),\n outputSchema: z.object({\n success: z.boolean(),\n }),\n errorCodes: [\"invalid_input\", \"session_invalid\", \"permission_denied\"],\n handler: async function ({ input, pathParams, headers, query }, { json, error }) {\n const { role } = await input.valid();\n const { userId } = pathParams;\n\n const sessionId = extractSessionId(headers, query.get(\"sessionId\"));\n\n if (!sessionId) {\n return error({ message: \"Session ID required\", code: \"session_invalid\" }, 400);\n }\n\n const [result] = await this.handlerTx()\n .withServiceCalls(() => [services.updateUserRoleWithSession(sessionId, userId, role)])\n .execute();\n\n if (!result.ok) {\n if (result.code === \"permission_denied\") {\n return error({ message: \"Unauthorized\", code: \"permission_denied\" }, 401);\n }\n\n return error({ message: \"Invalid session\", code: \"session_invalid\" }, 401);\n }\n\n return json({ success: true });\n },\n }),\n\n defineRoute({\n method: \"POST\",\n path: \"/sign-up\",\n inputSchema: z.object({\n email: z.email(),\n password: z.string().min(8).max(100),\n }),\n outputSchema: z.object({\n sessionId: z.string(),\n userId: z.string(),\n email: z.string(),\n role: z.enum([\"user\", \"admin\"]),\n }),\n errorCodes: [\"email_already_exists\", \"invalid_input\"],\n handler: async function ({ input }, { json, error }) {\n const { email, password } = await input.valid();\n\n const passwordHash = await hashPassword(password);\n\n const [result] = await this.handlerTx()\n .withServiceCalls(() => [services.signUpWithSession(email, passwordHash)])\n .execute();\n\n if (!result.ok) {\n return error({ message: \"Email already exists\", code: \"email_already_exists\" }, 400);\n }\n\n // Build response with Set-Cookie header\n const setCookieHeader = buildSetCookieHeader(result.sessionId, config.cookieOptions);\n\n return json(\n {\n sessionId: result.sessionId,\n userId: result.userId,\n email: result.email,\n role: result.role,\n },\n {\n headers: {\n \"Set-Cookie\": setCookieHeader,\n },\n },\n );\n },\n }),\n\n defineRoute({\n method: \"POST\",\n path: \"/sign-in\",\n inputSchema: z.object({\n email: z.email(),\n password: z.string().min(8).max(100),\n }),\n outputSchema: z.object({\n sessionId: z.string(),\n userId: z.string(),\n email: z.string(),\n role: z.enum([\"user\", \"admin\"]),\n }),\n errorCodes: [\"invalid_credentials\"],\n handler: async function ({ input }, { json, error }) {\n const { email, password } = await input.valid();\n\n // Get user by email\n const [user] = await this.handlerTx()\n .withServiceCalls(() => [services.getUserByEmail(email)])\n .execute();\n if (!user) {\n return error({ message: \"Invalid credentials\", code: \"invalid_credentials\" }, 401);\n }\n\n // Verify password\n const isValid = await verifyPassword(password, user.passwordHash);\n if (!isValid) {\n return error({ message: \"Invalid credentials\", code: \"invalid_credentials\" }, 401);\n }\n\n // Create session\n const [session] = await this.handlerTx()\n .withServiceCalls(() => [services.createSession(user.id)])\n .execute();\n\n // Build response with Set-Cookie header\n const setCookieHeader = buildSetCookieHeader(session.id, config.cookieOptions);\n\n return json(\n {\n sessionId: session.id,\n userId: user.id,\n email: user.email,\n role: user.role,\n },\n {\n headers: {\n \"Set-Cookie\": setCookieHeader,\n },\n },\n );\n },\n }),\n\n defineRoute({\n method: \"POST\",\n path: \"/change-password\",\n inputSchema: z.object({\n newPassword: z.string().min(8).max(100),\n }),\n outputSchema: z.object({\n success: z.boolean(),\n }),\n errorCodes: [\"session_invalid\"],\n handler: async function ({ input, headers, query }, { json, error }) {\n const { newPassword } = await input.valid();\n\n const sessionId = extractSessionId(headers, query.get(\"sessionId\"));\n\n if (!sessionId) {\n return error({ message: \"Session ID required\", code: \"session_invalid\" }, 400);\n }\n\n const passwordHash = await hashPassword(newPassword);\n\n const [result] = await this.handlerTx()\n .withServiceCalls(() => [services.changePasswordWithSession(sessionId, passwordHash)])\n .execute();\n\n if (!result.ok) {\n return error({ message: \"Invalid session\", code: \"session_invalid\" }, 401);\n }\n\n return json({ success: true });\n },\n }),\n ];\n },\n);\n","import { defineRoute, defineRoutes } from \"@fragno-dev/core\";\nimport type { DatabaseServiceContext } from \"@fragno-dev/db\";\nimport { authSchema } from \"../schema\";\nimport { z } from \"zod\";\nimport {\n buildClearCookieHeader,\n buildSetCookieHeader,\n extractSessionId,\n type CookieOptions,\n} from \"../utils/cookie\";\nimport type { Role, authFragmentDefinition } from \"..\";\n\ntype AuthServiceContext = DatabaseServiceContext<{}>;\n\nexport function createSessionServices(cookieOptions?: CookieOptions) {\n const services = {\n buildSessionCookie: function (sessionId: string): string {\n return buildSetCookieHeader(sessionId, cookieOptions);\n },\n createSession: function (this: AuthServiceContext, userId: string) {\n const expiresAt = new Date();\n expiresAt.setDate(expiresAt.getDate() + 30); // 30 days from now\n\n return this.serviceTx(authSchema)\n .mutate(({ uow }) => {\n const id = uow.create(\"session\", {\n userId,\n expiresAt,\n });\n\n return {\n id: id.valueOf(),\n userId,\n expiresAt,\n };\n })\n .build();\n },\n validateSession: function (this: AuthServiceContext, sessionId: string) {\n const now = new Date();\n return this.serviceTx(authSchema)\n .retrieve((uow) =>\n uow.findFirst(\"session\", (b) =>\n b\n .whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", sessionId))\n .join((j) => j.sessionOwner((b) => b.select([\"id\", \"email\", \"role\"]))),\n ),\n )\n .mutate(({ uow, retrieveResult: [session] }) => {\n if (!session) {\n return null;\n }\n\n // Check if session has expired\n if (session.expiresAt < now) {\n uow.delete(\"session\", session.id, (b) => b.check());\n return null;\n }\n\n if (!session.sessionOwner) {\n return null;\n }\n\n return {\n id: session.id.valueOf(),\n userId: session.userId as unknown as string,\n user: {\n id: session.sessionOwner.id.valueOf(),\n email: session.sessionOwner.email,\n role: session.sessionOwner.role,\n },\n };\n })\n .build();\n },\n invalidateSession: function (this: AuthServiceContext, sessionId: string) {\n return this.serviceTx(authSchema)\n .retrieve((uow) =>\n uow.findFirst(\"session\", (b) =>\n b.whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", sessionId)),\n ),\n )\n .mutate(({ uow, retrieveResult: [session] }) => {\n if (!session) {\n return false;\n }\n\n uow.delete(\"session\", session.id, (b) => b.check());\n return true;\n })\n .build();\n },\n getSession: function (this: AuthServiceContext, headers: Headers) {\n const sessionId = extractSessionId(headers);\n const now = new Date();\n\n return this.serviceTx(authSchema)\n .retrieve((uow) =>\n uow.findFirst(\"session\", (b) =>\n b\n .whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", sessionId ?? \"\"))\n .join((j) => j.sessionOwner((b) => b.select([\"id\", \"email\", \"role\"]))),\n ),\n )\n .mutate(({ uow, retrieveResult: [session] }) => {\n if (!session || !sessionId) {\n return undefined;\n }\n\n if (session.expiresAt < now) {\n uow.delete(\"session\", session.id, (b) => b.check());\n return undefined;\n }\n\n if (!session.sessionOwner) {\n return undefined;\n }\n\n return {\n userId: session.sessionOwner.id.valueOf(),\n email: session.sessionOwner.email,\n };\n })\n .build();\n },\n };\n return services;\n}\n\nexport const sessionRoutesFactory = defineRoutes<typeof authFragmentDefinition>().create(\n ({ services, config }) => {\n return [\n defineRoute({\n method: \"POST\",\n path: \"/sign-out\",\n inputSchema: z\n .object({\n sessionId: z.string().optional(),\n })\n .optional(),\n outputSchema: z.object({\n success: z.boolean(),\n }),\n errorCodes: [\"session_not_found\"],\n handler: async function ({ input, headers }, { json, error }) {\n const body = await input.valid();\n\n // Extract session ID from cookies first, then body\n const sessionId = extractSessionId(headers, null, body?.sessionId);\n\n if (!sessionId) {\n return error({ message: \"Session ID required\", code: \"session_not_found\" }, 400);\n }\n\n const [success] = await this.handlerTx()\n .withServiceCalls(() => [services.invalidateSession(sessionId)])\n .execute();\n\n // Build response with clear cookie header\n const clearCookieHeader = buildClearCookieHeader(config.cookieOptions ?? {});\n\n if (!success) {\n // Still clear the cookie even if session not found in DB\n return json(\n { success: false },\n {\n headers: {\n \"Set-Cookie\": clearCookieHeader,\n },\n },\n );\n }\n\n return json(\n { success: true },\n {\n headers: {\n \"Set-Cookie\": clearCookieHeader,\n },\n },\n );\n },\n }),\n\n defineRoute({\n method: \"GET\",\n path: \"/me\",\n queryParameters: [\"sessionId\"],\n outputSchema: z.object({\n userId: z.string(),\n email: z.string(),\n role: z.enum([\"user\", \"admin\"]),\n }),\n errorCodes: [\"session_invalid\"],\n handler: async function ({ query, headers }, { json, error }) {\n // Extract session ID from cookies first, then query params\n const sessionId = extractSessionId(headers, query.get(\"sessionId\"));\n\n if (!sessionId) {\n return error({ message: \"Session ID required\", code: \"session_invalid\" }, 400);\n }\n\n const [session] = await this.handlerTx()\n .withServiceCalls(() => [services.validateSession(sessionId)])\n .execute();\n\n if (!session) {\n return error({ message: \"Invalid session\", code: \"session_invalid\" }, 401);\n }\n\n return json({\n userId: session.user.id,\n email: session.user.email,\n role: session.user.role as Role,\n });\n },\n }),\n ];\n },\n);\n","import { defineRoute, defineRoutes } from \"@fragno-dev/core\";\nimport type { DatabaseServiceContext } from \"@fragno-dev/db\";\nimport { type Cursor, decodeCursor } from \"@fragno-dev/db/cursor\";\nimport { authSchema } from \"../schema\";\nimport { z } from \"zod\";\nimport type { authFragmentDefinition } from \"..\";\n\nexport type SortField = \"email\" | \"createdAt\";\nexport type SortOrder = \"asc\" | \"desc\";\n\nexport interface GetUsersParams {\n search?: string;\n sortBy: SortField;\n sortOrder: SortOrder;\n pageSize: number;\n cursor?: Cursor;\n}\n\nexport interface UserResult {\n id: string;\n email: string;\n role: \"user\" | \"admin\";\n createdAt: Date;\n}\n\ntype AuthServiceContext = DatabaseServiceContext<{}>;\n\nexport function createUserOverviewServices() {\n const mapUser = (user: {\n id: unknown;\n email: string;\n role: string;\n createdAt: Date;\n }): UserResult => ({\n id: String(user.id),\n email: user.email,\n role: user.role as \"user\" | \"admin\",\n createdAt: user.createdAt,\n });\n\n return {\n getUsersWithCursor: function (this: AuthServiceContext, params: GetUsersParams) {\n const { search, sortBy, sortOrder, pageSize, cursor } = params;\n\n // Determine which index to use based on search and sortBy\n // When searching, only email sorting is allowed (search uses email index)\n const effectiveSortBy: SortField = search ? \"email\" : sortBy;\n const indexName = effectiveSortBy === \"email\" ? \"idx_user_email\" : \"idx_user_createdAt\";\n\n // If cursor is provided, extract its metadata to ensure consistency\n const effectiveSortOrder = cursor ? cursor.orderDirection : sortOrder;\n const effectivePageSize = cursor ? cursor.pageSize : pageSize;\n\n return this.serviceTx(authSchema)\n .retrieve((uow) =>\n uow.findWithCursor(\"user\", (b) => {\n // When searching, we must filter by email and can only use the email index\n if (search) {\n const query = b\n .whereIndex(\"idx_user_email\", (eb) => eb(\"email\", \"contains\", search))\n .orderByIndex(\"idx_user_email\", effectiveSortOrder)\n .pageSize(effectivePageSize);\n\n // Add cursor for pagination continuation\n return cursor ? query.after(cursor) : query;\n }\n\n // When not searching, use the appropriate index for sorting\n const query = b\n .whereIndex(indexName)\n .orderByIndex(indexName, effectiveSortOrder)\n .pageSize(effectivePageSize);\n\n // Add cursor for pagination continuation\n return cursor ? query.after(cursor) : query;\n }),\n )\n .transformRetrieve(([result]) => ({\n users: result.items.map(mapUser),\n cursor: result.cursor,\n hasNextPage: result.hasNextPage,\n }))\n .build();\n },\n };\n}\n\nconst sortBySchema = z.enum([\"email\", \"createdAt\"]);\nconst sortOrderSchema = z.enum([\"asc\", \"desc\"]);\n\nexport const userOverviewRoutesFactory = defineRoutes<typeof authFragmentDefinition>().create(\n ({ services }) => {\n const querySchema = z.object({\n search: z.string().optional(),\n sortBy: sortBySchema.default(\"createdAt\"),\n sortOrder: sortOrderSchema.default(\"desc\"),\n pageSize: z.coerce.number().int().min(1).max(100).default(20),\n });\n\n const parseCursor = (cursorParam: string | null): Cursor | undefined => {\n if (!cursorParam) {\n return undefined;\n }\n try {\n return decodeCursor(cursorParam);\n } catch {\n return undefined;\n }\n };\n\n return [\n defineRoute({\n method: \"GET\",\n path: \"/users\",\n queryParameters: [\"search\", \"sortBy\", \"sortOrder\", \"pageSize\", \"cursor\"],\n outputSchema: z.object({\n users: z.array(\n z.object({\n id: z.string(),\n email: z.string(),\n role: z.enum([\"user\", \"admin\"]),\n createdAt: z.string(),\n }),\n ),\n cursor: z.string().optional(),\n hasNextPage: z.boolean(),\n sortBy: sortBySchema,\n }),\n errorCodes: [\"invalid_input\"],\n handler: async function ({ query }, { json, error }) {\n const parsed = querySchema.safeParse(Object.fromEntries(query.entries()));\n if (!parsed.success) {\n return error({ message: \"Invalid query parameters\", code: \"invalid_input\" }, 400);\n }\n\n const rawSearch = parsed.data.search?.trim();\n const search = rawSearch ? rawSearch : undefined;\n const sortBy: SortField = search ? \"email\" : parsed.data.sortBy;\n const params = {\n search,\n sortBy,\n sortOrder: parsed.data.sortOrder as SortOrder,\n pageSize: parsed.data.pageSize,\n };\n const cursor = parseCursor(query.get(\"cursor\"));\n\n const [result] = await this.handlerTx()\n .withServiceCalls(() => [services.getUsersWithCursor({ ...params, cursor })])\n .execute();\n\n return json({\n users: result.users.map((user) => ({\n id: user.id,\n email: user.email,\n role: user.role,\n createdAt: user.createdAt.toISOString(),\n })),\n cursor: result.cursor?.encode(),\n hasNextPage: result.hasNextPage,\n sortBy: params.sortBy, // Return the actual sortBy used (may differ from requested)\n });\n },\n }),\n ];\n },\n);\n","import { defineFragment, instantiate } from \"@fragno-dev/core\";\nimport { createClientBuilder, type FragnoPublicClientConfig } from \"@fragno-dev/core/client\";\nimport { withDatabase, type FragnoPublicConfigWithDatabase } from \"@fragno-dev/db\";\nimport { authSchema } from \"./schema\";\nimport { createUserServices, userActionsRoutesFactory } from \"./user/user-actions\";\nimport { createSessionServices, sessionRoutesFactory } from \"./session/session\";\nimport {\n createUserOverviewServices,\n userOverviewRoutesFactory,\n type GetUsersParams,\n type UserResult,\n type SortField,\n type SortOrder,\n} from \"./user/user-overview\";\nimport type { CookieOptions } from \"./utils/cookie\";\n\nexport interface AuthConfig {\n sendEmail?: (params: { to: string; subject: string; body: string }) => Promise<void>;\n cookieOptions?: CookieOptions;\n}\n\nexport const authFragmentDefinition = defineFragment<AuthConfig>(\"auth\")\n .extend(withDatabase(authSchema))\n .providesBaseService(({ defineService, config }) => {\n return defineService({\n ...createUserServices(),\n ...createSessionServices(config.cookieOptions),\n ...createUserOverviewServices(),\n });\n })\n .build();\n\nexport type AuthFragment = typeof authFragmentDefinition;\n\nexport function createAuthFragment(\n config: AuthConfig = {},\n fragnoConfig: FragnoPublicConfigWithDatabase,\n) {\n const options = {\n ...fragnoConfig,\n // Preserve legacy namespace to avoid changing physical table names.\n databaseNamespace:\n fragnoConfig.databaseNamespace !== undefined\n ? fragnoConfig.databaseNamespace\n : \"simple-auth-db\",\n };\n\n return instantiate(authFragmentDefinition)\n .withConfig(config)\n .withOptions(options)\n .withRoutes([userActionsRoutesFactory, sessionRoutesFactory, userOverviewRoutesFactory])\n .build();\n}\n\nexport function createAuthFragmentClients(fragnoConfig?: FragnoPublicClientConfig) {\n // Note: Cookies are automatically sent for same-origin requests by the browser.\n // For cross-origin requests, you may need to configure CORS headers on the server.\n const config = { ...fragnoConfig };\n\n const b = createClientBuilder(\n authFragmentDefinition,\n config,\n [userActionsRoutesFactory, sessionRoutesFactory, userOverviewRoutesFactory],\n {\n type: \"options\",\n options: {\n credentials: \"include\",\n },\n },\n );\n\n const useMe = b.createHook(\"/me\");\n const useSignUp = b.createMutator(\"POST\", \"/sign-up\");\n const useSignIn = b.createMutator(\"POST\", \"/sign-in\");\n const useSignOut = b.createMutator(\"POST\", \"/sign-out\", (invalidate) => {\n invalidate(\"GET\", \"/me\", {});\n invalidate(\"GET\", \"/users\", {});\n });\n const useUsers = b.createHook(\"/users\");\n const useUpdateUserRole = b.createMutator(\"PATCH\", \"/users/:userId/role\", (invalidate) => {\n invalidate(\"GET\", \"/users\", {});\n invalidate(\"GET\", \"/me\", {});\n });\n const useChangePassword = b.createMutator(\"POST\", \"/change-password\");\n\n return {\n // Reactive hooks - Auth\n useSignUp,\n useSignIn,\n useSignOut,\n useMe,\n useUsers,\n useUpdateUserRole,\n useChangePassword,\n\n // Non-reactive methods\n signIn: {\n email: async ({\n email,\n password,\n rememberMe: _rememberMe,\n }: {\n email: string;\n password: string;\n rememberMe?: boolean;\n }) => {\n // Note: rememberMe is accepted but not yet implemented on the backend\n return useSignIn.mutateQuery({\n body: {\n email,\n password,\n },\n });\n },\n },\n\n signUp: {\n email: async ({ email, password }: { email: string; password: string }) => {\n return useSignUp.mutateQuery({\n body: {\n email,\n password,\n },\n });\n },\n },\n\n signOut: (params?: { sessionId?: string }) => {\n return useSignOut.mutateQuery({\n body: params?.sessionId ? { sessionId: params.sessionId } : {},\n });\n },\n\n me: async (params?: { sessionId?: string }) => {\n if (params?.sessionId) {\n return useMe.query({ query: { sessionId: params.sessionId } });\n }\n\n return useMe.query();\n },\n };\n}\n\nexport type { FragnoRouteConfig } from \"@fragno-dev/core/api\";\nexport type { GetUsersParams, UserResult, SortField, SortOrder };\n\nexport type Role = \"user\" | \"admin\";\n"],"mappings":"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"}
|