@vonall/auth 0.5.0 → 0.6.0
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/CHANGELOG.md +6 -0
- package/README.md +14 -7
- package/dist/remote/app_user.remote.js +13 -13
- package/dist/remote/einladung.remote.js +15 -12
- package/dist/server/{registry.d.ts → context.d.ts} +13 -16
- package/dist/server/context.js +22 -0
- package/dist/server/create.d.ts +27 -22
- package/dist/server/create.js +50 -33
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- package/dist/server/loads.d.ts +14 -2
- package/dist/server/loads.js +19 -14
- package/dist/server/repositories/EinladungRepo.d.ts +4 -1
- package/dist/server/repositories/EinladungRepo.js +66 -73
- package/dist/server/repositories/UserRepo.d.ts +4 -1
- package/dist/server/repositories/UserRepo.js +49 -57
- package/dist/server/service/einladung.service.d.ts +4 -1
- package/dist/server/service/einladung.service.js +54 -49
- package/dist/server/service/mail.d.ts +16 -12
- package/dist/server/service/mail.js +32 -34
- package/dist/testing/index.d.ts +6 -8
- package/dist/testing/index.js +16 -19
- package/dist/ui/context.d.ts +6 -7
- package/dist/ui/context.js +15 -9
- package/package.json +3 -3
- package/dist/server/registry.js +0 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @vonall/auth
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#17](https://github.com/SebastianvonAllmen/vonall-libs/pull/17) [`f1d99ba`](https://github.com/SebastianvonAllmen/vonall-libs/commit/f1d99ba19b46d4f3a0eb8dd7ed595c9a513b6d0f) Thanks [@SebastianvonAllmen](https://github.com/SebastianvonAllmen)! - An explicit context instead of the registry singleton. `@vonall/auth`: `createAuth(options)` returns `{ options, register }`; `register(auth)` gives `{ ctx, sessionHandle, apiHandler }` and checks the instance's plugins against the schema's options (`admin` is read off the schema, no longer an option). The repositories, services and mails are functions of the context (`userRepo(ctx)`, `einladungRepo(ctx)`, `einladungService(ctx)`, `authMails(ctx)`); remote functions read it with `authContext()`, loads with `contextOf(locals)`; the session handle puts it into `event.locals.auth`. `setAuthUi({ authClient })` takes only the client: the app's name, locale and signup mode come through the page data of the package's loads. `testAuth(db)` returns the context, `createUser(ctx, email)`. `@vonall/mcp`: `createMcp({ auth: ctx, … })`, `mcpPlugins(origin)` replaces `mcpPluginOptions`/`CIMD_OPTIONS`, `agentRepo(ctx)`, `mcpContext()`, `agentsTab()` with its own strings; `ConsentPage` and `AgentsTab` take no `appName`/`locale`; `@vonall/auth` is a peer dependency.
|
|
8
|
+
|
|
3
9
|
## 0.5.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -13,16 +13,19 @@ export const { user, session, account, verification, twoFactor, rateLimit, einla
|
|
|
13
13
|
|
|
14
14
|
// src/lib/auth.ts: the wiring, once; the instance is built here so its types (every plugin's) are inferred here
|
|
15
15
|
import { ac, roles } from "@vonall/auth";
|
|
16
|
-
import {
|
|
16
|
+
import { createAuth } from "@vonall/auth/server";
|
|
17
17
|
import { betterAuth } from "better-auth";
|
|
18
18
|
import { admin } from "better-auth/plugins";
|
|
19
19
|
import { twoFactor } from "better-auth/plugins/two-factor";
|
|
20
20
|
import { sveltekitCookies } from "better-auth/svelte-kit";
|
|
21
|
+
const wiring = createAuth({ db, schema, secret: BETTER_AUTH_SECRET, origin: ORIGIN, appName: APP_NAME, locale: LOCALE, signupMode: SIGNUP_MODE, mailer, social: { github: { clientId, clientSecret } }, log: logger });
|
|
21
22
|
export const auth = betterAuth({
|
|
22
|
-
...
|
|
23
|
+
...wiring.options,
|
|
23
24
|
plugins: [twoFactor(), admin({ ac, roles }), /* the MCP endpoint's, from @vonall/mcp */ sveltekitCookies(getRequestEvent)],
|
|
24
25
|
});
|
|
25
|
-
|
|
26
|
+
// The context: everything with accounts outside a request takes it (createMcp, scripts, tests); inside a request it is
|
|
27
|
+
// `event.locals.auth`. register() checks the instance's plugins against the schema's options (admin, mcp).
|
|
28
|
+
export const { ctx, sessionHandle, apiHandler } = wiring.register(auth);
|
|
26
29
|
|
|
27
30
|
// src/lib/auth-client.ts createAuthClient({ baseURL: PUBLIC_ORIGIN, plugins: [twoFactorClient(), adminClient({ ac, roles })] }) (from "better-auth/svelte")
|
|
28
31
|
// src/hooks.server.ts sequence(securityHeaders, sessionHandle)
|
|
@@ -33,10 +36,13 @@ export const { handle: sessionHandle, apiHandler, providers } = registerAuth(aut
|
|
|
33
36
|
|
|
34
37
|
Entry points: `@vonall/auth` (types, validation schemas, `ac`/`roles` for the admin plugins, `isAdmin`, `mayInvite`, the
|
|
35
38
|
fresh-session helpers, the messages), `@vonall/auth/schema`, `@vonall/auth/server`, `@vonall/auth/remote` (the remote
|
|
36
|
-
functions a Product's components call), `@vonall/auth/testing` (`
|
|
39
|
+
functions a Product's components call), `@vonall/auth/testing` (`testAuth(db)` gives the context, `createUser(ctx)` an account, for db tests). `better-auth` is
|
|
37
40
|
this package's dependency; the Product imports its plugins from the same version. `@node-rs/argon2` (the password
|
|
38
41
|
hash, a native module) is a peer dependency: the Product lists it, so its server bundle can leave it external.
|
|
39
42
|
|
|
43
|
+
The server side is functions of the context: `userRepo(ctx)`, `einladungRepo(ctx)`, `einladungService(ctx)`, `authMails(ctx)`;
|
|
44
|
+
a remote function gets it with `authContext()`, a load with `contextOf(locals)`.
|
|
45
|
+
|
|
40
46
|
Strings come in English and German: `configureAuthMessages({ locale, overrides })`. A Product's own user-owned tables
|
|
41
47
|
reference `user.id` with `onDelete: "cascade"`, so deleting a Konto takes everything with it. The MCP endpoint's
|
|
42
48
|
better-auth plugins and rate limit rule come from `@vonall/mcp` through `plugins` and `rateLimitRules`.
|
|
@@ -44,17 +50,18 @@ better-auth plugins and rate limit rule come from `@vonall/mcp` through `plugins
|
|
|
44
50
|
## The pages and the settings (`@vonall/auth/ui`)
|
|
45
51
|
|
|
46
52
|
```svelte
|
|
47
|
-
<!-- src/routes/+layout.svelte: once, so the components know the client
|
|
53
|
+
<!-- src/routes/+layout.svelte: once, so the components know the client; the app's name, locale and signup mode
|
|
54
|
+
reach them through the page data of this package's loads (authLayoutLoad, homeLayoutLoad, einladungLoad) -->
|
|
48
55
|
<script lang="ts">
|
|
49
56
|
import { setAuthUi } from "@vonall/auth/ui";
|
|
50
|
-
setAuthUi({ authClient
|
|
57
|
+
setAuthUi({ authClient });
|
|
51
58
|
</script>
|
|
52
59
|
|
|
53
60
|
<!-- src/routes/auth/login/+page.svelte --> <LoginForm>{#snippet footer()}…the privacy line…{/snippet}</LoginForm>
|
|
54
61
|
<!-- src/routes/auth/signup/+page.svelte --> <RegisterForm />
|
|
55
62
|
<!-- src/routes/auth/reset-password --> <ResetPasswordForm />
|
|
56
63
|
<!-- src/routes/einladung/[token] --> <EinladungPage {data} tagline={APP_TAGLINE} />
|
|
57
|
-
<!-- src/routes/home/settings --> <SettingsPage extraTabs={[
|
|
64
|
+
<!-- src/routes/home/settings --> <SettingsPage extraTabs={[agentsTab()]}>{#snippet extra(id)}…{/snippet}</SettingsPage>
|
|
58
65
|
<!-- src/routes/home/manage_user --> <AdminView currentUserId={data.currentUserId} />
|
|
59
66
|
```
|
|
60
67
|
|
|
@@ -3,16 +3,16 @@ import { getRequestEvent } from "$app/server";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { m } from "../messages.js";
|
|
5
5
|
import { adminQuery, adminSchemaCommand, protectedCommand, protectedForm, protectedQuery, protectedSchemaCommand } from "../server/helpers.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { authContext } from "../server/context.js";
|
|
7
|
+
import { userRepo } from "../server/repositories/UserRepo.js";
|
|
8
8
|
import { emailSchema, nameSchema, passwordSchema } from "../types.js";
|
|
9
9
|
import { isSessionNotFresh, reauthPath } from "../util/fresh.js";
|
|
10
10
|
/** The Admin option: every account. */
|
|
11
11
|
export const getAllUsers = adminQuery(async () => {
|
|
12
|
-
return await
|
|
12
|
+
return await userRepo(authContext()).getAllUsers();
|
|
13
13
|
});
|
|
14
14
|
export const updateUserProfile = protectedForm(z.object({ name: nameSchema, email: emailSchema }), async (data, _issue, user) => {
|
|
15
|
-
const auth =
|
|
15
|
+
const { auth } = authContext();
|
|
16
16
|
const event = getRequestEvent();
|
|
17
17
|
if (data.email !== user.email) {
|
|
18
18
|
await auth.api.changeEmail({ body: { newEmail: data.email }, headers: event.request.headers });
|
|
@@ -24,7 +24,7 @@ const passwordPair = { newPassword: passwordSchema, confirmPassword: z.string()
|
|
|
24
24
|
const passwordsMatch = { error: () => m().passwordsMismatch, path: ["confirmPassword"] };
|
|
25
25
|
export const changePassword = protectedForm(z.object({ currentPassword: passwordSchema, ...passwordPair }).refine((d) => d.newPassword === d.confirmPassword, passwordsMatch), async (data) => {
|
|
26
26
|
const event = getRequestEvent();
|
|
27
|
-
await
|
|
27
|
+
await authContext().auth.api.changePassword({
|
|
28
28
|
body: { currentPassword: data.currentPassword, newPassword: data.newPassword, revokeOtherSessions: true },
|
|
29
29
|
headers: event.request.headers,
|
|
30
30
|
});
|
|
@@ -32,13 +32,13 @@ export const changePassword = protectedForm(z.object({ currentPassword: password
|
|
|
32
32
|
});
|
|
33
33
|
export const setPassword = protectedForm(z.object(passwordPair).refine((d) => d.newPassword === d.confirmPassword, passwordsMatch), async (data) => {
|
|
34
34
|
const event = getRequestEvent();
|
|
35
|
-
await
|
|
35
|
+
await authContext().auth.api.setPassword({ body: { newPassword: data.newPassword }, headers: event.request.headers });
|
|
36
36
|
return { success: true };
|
|
37
37
|
});
|
|
38
38
|
export const getSessions = protectedQuery(async () => {
|
|
39
39
|
const event = getRequestEvent();
|
|
40
40
|
try {
|
|
41
|
-
return await
|
|
41
|
+
return await authContext().auth.api.listSessions({ headers: event.request.headers });
|
|
42
42
|
}
|
|
43
43
|
catch (e) {
|
|
44
44
|
// Only a login from the last day may list the sessions; the sessions are in the settings
|
|
@@ -50,7 +50,7 @@ export const getSessions = protectedQuery(async () => {
|
|
|
50
50
|
export const revokeSessionById = protectedSchemaCommand(z.object({ sessionId: z.string() }), async (data) => {
|
|
51
51
|
const event = getRequestEvent();
|
|
52
52
|
const currentSessionToken = event.locals.session?.token;
|
|
53
|
-
await
|
|
53
|
+
await authContext().auth.api.revokeSession({ body: { token: data.sessionId }, headers: event.request.headers });
|
|
54
54
|
const isCurrentSession = data.sessionId === currentSessionToken;
|
|
55
55
|
if (!isCurrentSession)
|
|
56
56
|
await getSessions().refresh();
|
|
@@ -58,19 +58,19 @@ export const revokeSessionById = protectedSchemaCommand(z.object({ sessionId: z.
|
|
|
58
58
|
});
|
|
59
59
|
export const revokeOthers = protectedCommand(async () => {
|
|
60
60
|
const event = getRequestEvent();
|
|
61
|
-
await
|
|
61
|
+
await authContext().auth.api.revokeOtherSessions({ headers: event.request.headers });
|
|
62
62
|
await getSessions().refresh();
|
|
63
63
|
return { success: true };
|
|
64
64
|
});
|
|
65
65
|
export const getLinkedAccounts = protectedQuery(async (user) => {
|
|
66
|
-
return await
|
|
66
|
+
return await userRepo(authContext()).getUserAccounts(user.id);
|
|
67
67
|
});
|
|
68
68
|
/** The Admin option. */
|
|
69
69
|
export const setUserRole = adminSchemaCommand(z.object({ userId: z.string(), role: z.enum(["admin", "user"]) }), async ({ userId, role }, admin) => {
|
|
70
70
|
if (userId === admin.id) {
|
|
71
71
|
throw error(400, { message: m().ownRole, id: "own_role", timestamp: new Date().toISOString() });
|
|
72
72
|
}
|
|
73
|
-
await
|
|
73
|
+
await userRepo(authContext()).setRole(userId, role);
|
|
74
74
|
await getAllUsers().refresh();
|
|
75
75
|
return { success: true };
|
|
76
76
|
});
|
|
@@ -79,12 +79,12 @@ export const deleteUser = adminSchemaCommand(z.object({ userId: z.string() }), a
|
|
|
79
79
|
if (userId === admin.id) {
|
|
80
80
|
throw error(400, { message: m().ownAccount, id: "own_account", timestamp: new Date().toISOString() });
|
|
81
81
|
}
|
|
82
|
-
await
|
|
82
|
+
await userRepo(authContext()).kontoLoeschen(userId);
|
|
83
83
|
await getAllUsers().refresh();
|
|
84
84
|
return { success: true };
|
|
85
85
|
});
|
|
86
86
|
/** Deletes the user's own account with all its data. */
|
|
87
87
|
export const deleteOwnAccount = protectedCommand(async (user) => {
|
|
88
|
-
await
|
|
88
|
+
await userRepo(authContext()).kontoLoeschen(user.id);
|
|
89
89
|
return { success: true };
|
|
90
90
|
});
|
|
@@ -2,10 +2,10 @@ import { error } from "@sveltejs/kit";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { m } from "../messages.js";
|
|
4
4
|
import { adminQuery, adminSchemaCommand, protectedQuery, protectedSchemaCommand } from "../server/helpers.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { authContext } from "../server/context.js";
|
|
6
|
+
import { einladungRepo } from "../server/repositories/EinladungRepo.js";
|
|
7
|
+
import { einladungService } from "../server/service/einladung.service.js";
|
|
8
|
+
import { authMails } from "../server/service/mail.js";
|
|
9
9
|
import { emailSchema } from "../types.js";
|
|
10
10
|
import { messageOf } from "../util/messageOf.js";
|
|
11
11
|
import { isAdmin, mayInvite } from "../util/signup.js";
|
|
@@ -16,20 +16,22 @@ function unwrap(result) {
|
|
|
16
16
|
}
|
|
17
17
|
/** The user's open Einladungen, and whether the server can send them by email. */
|
|
18
18
|
export const getOffeneEinladungen = protectedQuery(async (user) => {
|
|
19
|
-
|
|
19
|
+
const ctx = authContext();
|
|
20
|
+
return { einladungen: await einladungRepo(ctx).listOpenBy(user.id, new Date()), mail: ctx.mailer.enabled };
|
|
20
21
|
});
|
|
21
22
|
/** Creates an Einladung; the link is returned only here. With an email, it is also sent (if mail is set up). */
|
|
22
23
|
export const createEinladung = protectedSchemaCommand(z.object({ email: emailSchema.nullable() }), async ({ email }, user) => {
|
|
23
|
-
const
|
|
24
|
+
const ctx = authContext();
|
|
25
|
+
const { origin, signupMode, admin } = ctx;
|
|
24
26
|
if (!mayInvite(signupMode, isAdmin(user))) {
|
|
25
27
|
throw error(403, { message: m().onlyAdminsInvite, id: "forbidden", timestamp: new Date().toISOString() });
|
|
26
28
|
}
|
|
27
|
-
const einladung = await
|
|
29
|
+
const einladung = await einladungService(ctx).erstellen(user.id, { email });
|
|
28
30
|
const link = `${origin}/einladung/${einladung.token}`;
|
|
29
31
|
let mailFehler = null;
|
|
30
32
|
if (email) {
|
|
31
33
|
try {
|
|
32
|
-
await
|
|
34
|
+
await authMails(ctx).einladung(user.name, email, { link, expiresAt: einladung.expiresAt });
|
|
33
35
|
}
|
|
34
36
|
catch (e) {
|
|
35
37
|
mailFehler = messageOf(e, m().mailFailed);
|
|
@@ -41,17 +43,18 @@ export const createEinladung = protectedSchemaCommand(z.object({ email: emailSch
|
|
|
41
43
|
return { link, expiresAt: einladung.expiresAt, mailFehler };
|
|
42
44
|
});
|
|
43
45
|
export const withdrawEinladung = protectedSchemaCommand(z.object({ einladungId: z.string() }), async ({ einladungId }, user) => {
|
|
44
|
-
|
|
46
|
+
const ctx = authContext();
|
|
47
|
+
unwrap(await einladungService(ctx).zurueckziehen(user.id, einladungId));
|
|
45
48
|
await getOffeneEinladungen().refresh();
|
|
46
|
-
if (
|
|
49
|
+
if (ctx.admin && isAdmin(user))
|
|
47
50
|
await getAlleEinladungen().refresh();
|
|
48
51
|
});
|
|
49
52
|
/** The Admin option: every open Einladung, and withdrawing any of them. */
|
|
50
53
|
export const getAlleEinladungen = adminQuery(async () => {
|
|
51
|
-
return await
|
|
54
|
+
return await einladungRepo(authContext()).listAllOpen(new Date());
|
|
52
55
|
});
|
|
53
56
|
export const adminWithdrawEinladung = adminSchemaCommand(z.object({ einladungId: z.string() }), async ({ einladungId }) => {
|
|
54
|
-
unwrap(await
|
|
57
|
+
unwrap(await einladungService(authContext()).zurueckziehen(null, einladungId));
|
|
55
58
|
await getAlleEinladungen().refresh();
|
|
56
59
|
await getOffeneEinladungen().refresh();
|
|
57
60
|
});
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What `createAuth()` set up, for the package's repositories, services and remote functions: the Product's database,
|
|
3
|
-
* its tables, the options and the better-auth instance. One app per process, so a module-level value; the Product's
|
|
4
|
-
* wiring (`src/lib/auth.ts`) runs before any request.
|
|
5
|
-
*/
|
|
6
1
|
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
7
2
|
import type { Mailer } from "@vonall/mail";
|
|
8
|
-
import type {
|
|
3
|
+
import type { AuthSchema } from "../schema/index.js";
|
|
9
4
|
import type { Auth } from "./create.js";
|
|
10
5
|
import type { SignupMode } from "../types.js";
|
|
11
6
|
export type Db = NodePgDatabase<any>;
|
|
@@ -16,27 +11,29 @@ export type AuthLog = {
|
|
|
16
11
|
warn(message: string, context?: Record<string, unknown>): void;
|
|
17
12
|
error(message: string, context?: Record<string, unknown>, error?: unknown): void;
|
|
18
13
|
};
|
|
19
|
-
export type
|
|
14
|
+
export type AuthContext<S extends AuthSchema = AuthSchema> = {
|
|
20
15
|
db: Db;
|
|
21
|
-
schema:
|
|
16
|
+
schema: S;
|
|
22
17
|
appName: string;
|
|
23
18
|
origin: string;
|
|
24
|
-
/** The app's locale
|
|
19
|
+
/** The app's locale, e.g. "de-CH": dates in mails and on the pages. */
|
|
25
20
|
locale: string;
|
|
26
21
|
signupMode: SignupMode;
|
|
27
22
|
mailer: Mailer;
|
|
28
|
-
admin: boolean;
|
|
29
23
|
log: AuthLog;
|
|
30
|
-
/** The
|
|
31
|
-
|
|
24
|
+
/** The Admin option: `schema` has the role columns (and the instance the admin plugin, `register()` checks). */
|
|
25
|
+
admin: boolean;
|
|
26
|
+
/** Whether `schema` has the OAuth provider's tables (`@vonall/mcp`). */
|
|
27
|
+
mcp: boolean;
|
|
32
28
|
/** The social logins that have keys. */
|
|
33
29
|
providers: {
|
|
34
30
|
github: boolean;
|
|
35
31
|
google: boolean;
|
|
36
32
|
};
|
|
33
|
+
/** The Product's instance; reading it before `register()` throws. */
|
|
34
|
+
readonly auth: Auth;
|
|
37
35
|
};
|
|
38
|
-
|
|
39
|
-
export declare function
|
|
40
|
-
|
|
41
|
-
export declare function authInstance(): Auth;
|
|
36
|
+
/** The context of the current request: `event.locals.auth`, which the session handle set. */
|
|
37
|
+
export declare function authContext(): AuthContext;
|
|
38
|
+
export declare function contextOf(locals: object): AuthContext;
|
|
42
39
|
export declare const consoleLog: AuthLog;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `createAuth()` set up, for everything else in this package and in `@vonall/mcp`: the Product's database and
|
|
3
|
+
* tables, its options and, after `register()`, its better-auth instance. It is passed explicitly; inside a request it
|
|
4
|
+
* is also in `event.locals.auth` (the session handle puts it there), where the remote functions and the loads find it.
|
|
5
|
+
*/
|
|
6
|
+
import { getRequestEvent } from "$app/server";
|
|
7
|
+
/** The context of the current request: `event.locals.auth`, which the session handle set. */
|
|
8
|
+
export function authContext() {
|
|
9
|
+
return contextOf(getRequestEvent().locals);
|
|
10
|
+
}
|
|
11
|
+
export function contextOf(locals) {
|
|
12
|
+
const ctx = locals.auth;
|
|
13
|
+
if (!ctx)
|
|
14
|
+
throw new Error("@vonall/auth: no auth context on this request; the session handle of createAuth().register() is not in hooks.server.ts");
|
|
15
|
+
return ctx;
|
|
16
|
+
}
|
|
17
|
+
export const consoleLog = {
|
|
18
|
+
debug: () => { },
|
|
19
|
+
info: (message, context) => console.info(message, context ?? ""),
|
|
20
|
+
warn: (message, context) => console.warn(message, context ?? ""),
|
|
21
|
+
error: (message, context, error) => console.error(message, context ?? "", error ?? ""),
|
|
22
|
+
};
|
package/dist/server/create.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* better-auth for a Product
|
|
3
|
-
* types are inferred
|
|
4
|
-
* `
|
|
2
|
+
* better-auth for a Product: `const wiring = createAuth({…})`, then the Product builds the instance itself, so its
|
|
3
|
+
* types are inferred there (its plugins included): `betterAuth({ ...wiring.options, plugins: […] })`, and
|
|
4
|
+
* `wiring.register(auth)` gives the context, the session handle and the API handler; everything else in this package
|
|
5
|
+
* takes the context.
|
|
5
6
|
*/
|
|
6
7
|
import type { RequestHandler } from "@sveltejs/kit";
|
|
7
8
|
import type { Handle } from "@sveltejs/kit/hooks";
|
|
@@ -12,29 +13,27 @@ import { sveltekitCookies } from "better-auth/svelte-kit";
|
|
|
12
13
|
import type { Mailer } from "@vonall/mail";
|
|
13
14
|
import { roles, ac } from "../permissions.js";
|
|
14
15
|
import type { AuthSchema } from "../schema/index.js";
|
|
15
|
-
import { type AuthLog, type Db } from "./
|
|
16
|
+
import { type AuthContext, type AuthLog, type Db } from "./context.js";
|
|
16
17
|
import { type SignupMode } from "../types.js";
|
|
17
18
|
export type SocialProvider = {
|
|
18
19
|
clientId: string;
|
|
19
20
|
clientSecret: string;
|
|
20
21
|
};
|
|
21
|
-
export type CreateAuthOptions = {
|
|
22
|
+
export type CreateAuthOptions<S extends AuthSchema = AuthSchema> = {
|
|
22
23
|
/** The Product's Drizzle database. */
|
|
23
24
|
db: Db;
|
|
24
|
-
/** The Product's tables from `authSchema()`, the same objects its `schema/` re-exports. */
|
|
25
|
-
schema:
|
|
25
|
+
/** The Product's tables from `authSchema()`, the same objects its `schema/` re-exports; its options are read off them. */
|
|
26
|
+
schema: S;
|
|
26
27
|
/** BETTER_AUTH_SECRET. */
|
|
27
28
|
secret: string;
|
|
28
29
|
/** The app's own address (ORIGIN), e.g. https://blueprint.vonall.ch. */
|
|
29
30
|
origin: string;
|
|
30
31
|
appName: string;
|
|
31
|
-
/** LOCALE, e.g. "en-GB" or "de-CH": dates in mails. */
|
|
32
|
+
/** LOCALE, e.g. "en-GB" or "de-CH": dates in mails and on the pages. */
|
|
32
33
|
locale: string;
|
|
33
34
|
signupMode: SignupMode;
|
|
34
35
|
/** The Product's mailer (`@vonall/mail`); off without SMTP, which the signup mode `open` and the reset respect. */
|
|
35
36
|
mailer: Mailer;
|
|
36
|
-
/** The Admin option: the admin plugin and roles, the first account becomes admin. Matches `authSchema({ admin })`. */
|
|
37
|
-
admin?: boolean;
|
|
38
37
|
/** Social logins with keys; a provider without keys is left out (its button never shows). */
|
|
39
38
|
social?: {
|
|
40
39
|
github?: SocialProvider | null;
|
|
@@ -60,13 +59,18 @@ export type Auth<P extends BetterAuthPlugin[] = []> = ReturnType<typeof betterAu
|
|
|
60
59
|
plugins: [...BasePlugins, ...P];
|
|
61
60
|
}>>;
|
|
62
61
|
/**
|
|
63
|
-
* The Product's instance as `
|
|
62
|
+
* The Product's instance as `register()` takes it: built on `wiring.options`, with any plugins (their types stay the
|
|
64
63
|
* Product's; this package uses the instance through its own `Auth` type).
|
|
65
64
|
*/
|
|
66
65
|
export type AuthLike = {
|
|
67
66
|
handler: (request: Request) => Promise<Response>;
|
|
68
67
|
api: object;
|
|
69
68
|
$context: Promise<object>;
|
|
69
|
+
options: {
|
|
70
|
+
plugins?: {
|
|
71
|
+
id: string;
|
|
72
|
+
}[];
|
|
73
|
+
};
|
|
70
74
|
};
|
|
71
75
|
/** The admin plugin's columns, named here because the plugin's type doesn't carry them into the session's user. */
|
|
72
76
|
export type AdminFields = {
|
|
@@ -78,18 +82,19 @@ export type AdminFields = {
|
|
|
78
82
|
/** The user and the session as the hooks put them into `locals` (a Product's `app.d.ts` uses these). */
|
|
79
83
|
export type AuthUser = Auth["$Infer"]["Session"]["user"] & AdminFields;
|
|
80
84
|
export type AuthSession = Auth["$Infer"]["Session"]["session"];
|
|
81
|
-
export
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
handle:
|
|
85
|
+
export type RegisteredAuth<S extends AuthSchema = AuthSchema> = {
|
|
86
|
+
/** For everything that works with accounts outside a request: `createMcp`, scripts, tests. */
|
|
87
|
+
ctx: AuthContext<S>;
|
|
88
|
+
/** The hooks handle: the context into `locals.auth`, the session into `locals.user`/`locals.session`, and better-auth's own routes (/api/auth/*). */
|
|
89
|
+
sessionHandle: Handle;
|
|
85
90
|
/** For `src/routes/api/auth/[...auth]/+server.ts`: `export const GET = apiHandler, POST = apiHandler`. */
|
|
86
91
|
apiHandler: RequestHandler;
|
|
87
|
-
/** The social logins that have keys; the login, signup and settings pages show only these. */
|
|
88
|
-
providers: {
|
|
89
|
-
github: boolean;
|
|
90
|
-
google: boolean;
|
|
91
|
-
};
|
|
92
92
|
};
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
export type AuthWiring<S extends AuthSchema = AuthSchema> = {
|
|
94
|
+
/** For `betterAuth({ ...options, plugins: […] })`. */
|
|
95
|
+
options: BetterAuthOptions;
|
|
96
|
+
/** Hands the Product's instance to this package; checks its plugins against the schema's options. */
|
|
97
|
+
register(auth: AuthLike): RegisteredAuth<S>;
|
|
98
|
+
};
|
|
99
|
+
export declare function createAuth<S extends AuthSchema>(options: CreateAuthOptions<S>): AuthWiring<S>;
|
|
95
100
|
export {};
|
package/dist/server/create.js
CHANGED
|
@@ -7,11 +7,11 @@ import { twoFactor } from "better-auth/plugins/two-factor";
|
|
|
7
7
|
import { sveltekitCookies, svelteKitHandler } from "better-auth/svelte-kit";
|
|
8
8
|
import { m } from "../messages.js";
|
|
9
9
|
import { roles, ac } from "../permissions.js";
|
|
10
|
+
import { consoleLog } from "./context.js";
|
|
10
11
|
import { hashPassword, verifyPassword } from "./crypto.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { sendPasswordResetEmail, sendVerifyEmail } from "./service/mail.js";
|
|
12
|
+
import { userRepo } from "./repositories/UserRepo.js";
|
|
13
|
+
import { EINLADUNG_COOKIE, einladungService } from "./service/einladung.service.js";
|
|
14
|
+
import { authMails } from "./service/mail.js";
|
|
15
15
|
import { MIN_PASSWORD_LENGTH } from "../types.js";
|
|
16
16
|
import { nameFromEmail } from "../util/names.js";
|
|
17
17
|
/** The Einladung token the browser carries from its link (`/einladung/[token]`), if any. */
|
|
@@ -24,9 +24,10 @@ function einladungAusCookie() {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
const hasKeys = (p) => Boolean(p?.clientId && p.clientSecret && p.clientId !== "dummy");
|
|
27
|
-
export function
|
|
28
|
-
const { secret, origin, appName, signupMode,
|
|
29
|
-
|
|
27
|
+
export function createAuth(options) {
|
|
28
|
+
const { secret, origin, appName, signupMode, social = {}, rateLimitRules = {}, log = consoleLog } = options;
|
|
29
|
+
let instance = null;
|
|
30
|
+
const ctx = {
|
|
30
31
|
db: options.db,
|
|
31
32
|
schema: options.schema,
|
|
32
33
|
appName,
|
|
@@ -34,13 +35,21 @@ export function authOptions(options) {
|
|
|
34
35
|
locale: options.locale,
|
|
35
36
|
signupMode,
|
|
36
37
|
mailer: options.mailer,
|
|
37
|
-
admin,
|
|
38
38
|
log,
|
|
39
|
-
|
|
39
|
+
admin: "role" in options.schema.user,
|
|
40
|
+
mcp: "oauthClient" in options.schema,
|
|
40
41
|
providers: { github: hasKeys(social.github), google: hasKeys(social.google) },
|
|
41
|
-
|
|
42
|
+
get auth() {
|
|
43
|
+
if (!instance)
|
|
44
|
+
throw new Error("@vonall/auth: createAuth().register(auth) has not run; the app's auth wiring (src/lib/auth.ts) calls it after betterAuth()");
|
|
45
|
+
return instance;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const users = userRepo(ctx);
|
|
49
|
+
const einladungen = einladungService(ctx);
|
|
50
|
+
const mails = authMails(ctx);
|
|
42
51
|
const baseURL = origin || "http://localhost";
|
|
43
|
-
|
|
52
|
+
const betterAuthOptions = {
|
|
44
53
|
appName,
|
|
45
54
|
baseURL,
|
|
46
55
|
secret: secret || "dummy-secret-for-build-purposes-only",
|
|
@@ -73,7 +82,7 @@ export function authOptions(options) {
|
|
|
73
82
|
before: async (user) => {
|
|
74
83
|
// The signup mode decides: an Einladung, or (open) mail for the confirmation link. The very first account
|
|
75
84
|
// (the admin) needs neither and counts as confirmed.
|
|
76
|
-
const { state, firstAccount } = await
|
|
85
|
+
const { state, firstAccount } = await einladungen.signup(einladungAusCookie());
|
|
77
86
|
if (state === "needs_einladung")
|
|
78
87
|
throw new APIError("FORBIDDEN", { message: m().needsEinladung });
|
|
79
88
|
if (state === "no_mail")
|
|
@@ -87,14 +96,14 @@ export function authOptions(options) {
|
|
|
87
96
|
},
|
|
88
97
|
after: async (user) => {
|
|
89
98
|
// The first user becomes admin
|
|
90
|
-
if (admin && (await
|
|
99
|
+
if (ctx.admin && (await users.countUsers()) === 1) {
|
|
91
100
|
log.info("First user created, bootstrapping global role to admin");
|
|
92
|
-
await
|
|
101
|
+
await users.setRole(user.id, "admin");
|
|
93
102
|
}
|
|
94
103
|
const token = einladungAusCookie();
|
|
95
104
|
if (!token)
|
|
96
105
|
return;
|
|
97
|
-
const angenommen = await
|
|
106
|
+
const angenommen = await einladungen.annehmen(user.id, token);
|
|
98
107
|
if (angenommen.isErr())
|
|
99
108
|
log.warn("Einladung could not be accepted after signup", { userId: user.id, kind: angenommen.error.kind });
|
|
100
109
|
getRequestEvent().cookies.delete(EINLADUNG_COOKIE, { path: "/" });
|
|
@@ -109,7 +118,7 @@ export function authOptions(options) {
|
|
|
109
118
|
// Signup mode open: anyone may sign up, so the email address is confirmed before the account can be used
|
|
110
119
|
requireEmailVerification: signupMode === "open",
|
|
111
120
|
sendResetPassword: async ({ user, url }) => {
|
|
112
|
-
await
|
|
121
|
+
await mails.passwordReset(user, url);
|
|
113
122
|
},
|
|
114
123
|
},
|
|
115
124
|
emailVerification: {
|
|
@@ -118,7 +127,7 @@ export function authOptions(options) {
|
|
|
118
127
|
autoSignInAfterVerification: true,
|
|
119
128
|
sendVerificationEmail: async ({ user, url }) => {
|
|
120
129
|
if (!user.emailVerified)
|
|
121
|
-
await
|
|
130
|
+
await mails.verify(user, url);
|
|
122
131
|
},
|
|
123
132
|
},
|
|
124
133
|
socialProviders: {
|
|
@@ -134,21 +143,29 @@ export function authOptions(options) {
|
|
|
134
143
|
cookieCache: { enabled: false },
|
|
135
144
|
},
|
|
136
145
|
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
return {
|
|
147
|
+
options: betterAuthOptions,
|
|
148
|
+
register(auth) {
|
|
149
|
+
// The instance's plugins must match the schema's options, or the columns and the plugin disagree at runtime
|
|
150
|
+
const plugins = new Set((auth.options.plugins ?? []).map((p) => p.id));
|
|
151
|
+
if (plugins.has("admin") !== ctx.admin) {
|
|
152
|
+
throw new Error(`@vonall/auth: ${ctx.admin ? "authSchema({ admin: true }) but the instance has no admin plugin" : "the instance has the admin plugin but authSchema() was built without admin"}`);
|
|
153
|
+
}
|
|
154
|
+
if (plugins.has("oauth-provider") && !ctx.mcp)
|
|
155
|
+
throw new Error("@vonall/auth: the instance has the MCP plugin but authSchema() was built without mcp (its tables are missing)");
|
|
156
|
+
instance = auth;
|
|
157
|
+
const sessionHandle = async ({ event, resolve }) => {
|
|
158
|
+
event.locals.auth = ctx;
|
|
159
|
+
const session = await ctx.auth.api.getSession({ headers: event.request.headers });
|
|
160
|
+
if (session) {
|
|
161
|
+
event.locals.session = session.session;
|
|
162
|
+
event.locals.user = session.user;
|
|
163
|
+
}
|
|
164
|
+
const { building } = await import("$app/env");
|
|
165
|
+
return svelteKitHandler({ event, resolve, auth: ctx.auth, building });
|
|
166
|
+
};
|
|
167
|
+
const apiHandler = ({ request }) => auth.handler(request);
|
|
168
|
+
return { ctx, sessionHandle, apiHandler };
|
|
169
|
+
},
|
|
151
170
|
};
|
|
152
|
-
const apiHandler = ({ request }) => auth.handler(request);
|
|
153
|
-
return { handle, apiHandler, providers: current.providers };
|
|
154
171
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { authContext, contextOf, type AuthContext, type AuthLog, type Db } from "./context.js";
|
|
2
|
+
export { createAuth, type AdminFields, type Auth, type AuthLike, type AuthSession, type AuthUser, type AuthWiring, type CreateAuthOptions, type RegisteredAuth, type SocialProvider } from "./create.js";
|
|
2
3
|
export { adminCommand, adminQuery, adminSchemaCommand, adminSchemaQuery, protectedCommand, protectedForm, protectedQuery, protectedSchemaCommand, protectedSchemaQuery, requireAdmin, requireUser, } from "./helpers.js";
|
|
3
|
-
export { authLayoutLoad, einladungLoad, homeLayoutLoad, loginLoad, manageUserLoad, signupLoad } from "./loads.js";
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export { sendEinladungEmail, sendPasswordResetEmail, sendVerifyEmail } from "./service/mail.js";
|
|
4
|
+
export { authLayoutLoad, einladungLoad, homeLayoutLoad, loginLoad, manageUserLoad, signupLoad, type AuthUiData } from "./loads.js";
|
|
5
|
+
export { einladungRepo, type EinladungRepo } from "./repositories/EinladungRepo.js";
|
|
6
|
+
export { userRepo, type UserRepo } from "./repositories/UserRepo.js";
|
|
7
|
+
export { EINLADUNG_COOKIE, EINLADUNG_GUELTIG_TAGE, einladungService, type EinladungError, type EinladungService } from "./service/einladung.service.js";
|
|
8
|
+
export { authMails, type AuthMails } from "./service/mail.js";
|
package/dist/server/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { authContext, contextOf } from "./context.js";
|
|
2
|
+
export { createAuth } from "./create.js";
|
|
2
3
|
export { adminCommand, adminQuery, adminSchemaCommand, adminSchemaQuery, protectedCommand, protectedForm, protectedQuery, protectedSchemaCommand, protectedSchemaQuery, requireAdmin, requireUser, } from "./helpers.js";
|
|
3
4
|
export { authLayoutLoad, einladungLoad, homeLayoutLoad, loginLoad, manageUserLoad, signupLoad } from "./loads.js";
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export { sendEinladungEmail, sendPasswordResetEmail, sendVerifyEmail } from "./service/mail.js";
|
|
5
|
+
export { einladungRepo } from "./repositories/EinladungRepo.js";
|
|
6
|
+
export { userRepo } from "./repositories/UserRepo.js";
|
|
7
|
+
export { EINLADUNG_COOKIE, EINLADUNG_GUELTIG_TAGE, einladungService } from "./service/einladung.service.js";
|
|
8
|
+
export { authMails } from "./service/mail.js";
|