@camstack/addon-auth 1.1.1
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/assets/oidc/icon.svg +1 -0
- package/assets/webauthn/icon.svg +1 -0
- package/dist/chunk-Cek0wNdY.js +64 -0
- package/dist/dist-CXCR7sEk.js +22888 -0
- package/dist/dist-JmuFs-U5.mjs +22859 -0
- package/dist/magic-link/auth-magic-link.addon.js +240 -0
- package/dist/magic-link/auth-magic-link.addon.mjs +234 -0
- package/dist/oidc/auth-oidc.addon.js +1577 -0
- package/dist/oidc/auth-oidc.addon.mjs +1570 -0
- package/dist/webauthn/auth-webauthn.addon.js +12534 -0
- package/dist/webauthn/auth-webauthn.addon.mjs +12559 -0
- package/package.json +130 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
require("../chunk-Cek0wNdY.js");
|
|
6
|
+
const require_dist = require("../dist-CXCR7sEk.js");
|
|
7
|
+
//#region src/magic-link/auth-magic-link.addon.ts
|
|
8
|
+
/**
|
|
9
|
+
* Magic-link authentication addon.
|
|
10
|
+
*
|
|
11
|
+
* Passwordless flow:
|
|
12
|
+
* 1. Operator types their email on the login page → clicks "Send link".
|
|
13
|
+
* 2. The addon's `/request` route accepts the email, looks up the user
|
|
14
|
+
* via `user-management.listUsers()`, mints a short-lived HMAC bridge
|
|
15
|
+
* token (via `sso-bridge` cap), and delivers a URL of the form
|
|
16
|
+
* `https://<hub>/addon/auth-magic-link/login?bridge=<token>` to the
|
|
17
|
+
* user's email.
|
|
18
|
+
* 3. Email click hits `/login?bridge=…` → 302 to `/api/auth/sso/finish?bridge=…`
|
|
19
|
+
* → hub verifies the bridge token and mints the real session.
|
|
20
|
+
*
|
|
21
|
+
* Delivery is pluggable. This addon ships a "log-only" mode by default
|
|
22
|
+
* (writes the URL to the addon log so operators can copy it during
|
|
23
|
+
* setup before SMTP is configured). Production deployments configure
|
|
24
|
+
* the SMTP relay via global settings.
|
|
25
|
+
*
|
|
26
|
+
* Open follow-ups:
|
|
27
|
+
* - Wire `nodemailer` for real SMTP delivery (peer dep; the framework
|
|
28
|
+
* declares `smtpProvider` cap if/when we ship an SMTP addon).
|
|
29
|
+
* - Throttle requests per-email (rate-limit window).
|
|
30
|
+
* - Auto-provision unknown emails (currently rejected for safety).
|
|
31
|
+
*/
|
|
32
|
+
var DEFAULT_CONFIG = {
|
|
33
|
+
displayName: "Email magic link",
|
|
34
|
+
icon: "mail",
|
|
35
|
+
publicOrigin: "",
|
|
36
|
+
deliveryMode: "log-only",
|
|
37
|
+
defaultRole: "viewer",
|
|
38
|
+
ttlSec: 600
|
|
39
|
+
};
|
|
40
|
+
var AuthMagicLinkAddon = class extends require_dist.BaseAddon {
|
|
41
|
+
kind = "magic-link";
|
|
42
|
+
hasRedirectFlow = false;
|
|
43
|
+
hasCredentialFlow = true;
|
|
44
|
+
displayName;
|
|
45
|
+
icon;
|
|
46
|
+
constructor() {
|
|
47
|
+
super({ ...DEFAULT_CONFIG });
|
|
48
|
+
this.displayName = DEFAULT_CONFIG.displayName;
|
|
49
|
+
this.icon = DEFAULT_CONFIG.icon;
|
|
50
|
+
}
|
|
51
|
+
async onInitialize() {
|
|
52
|
+
Object.assign(this, {
|
|
53
|
+
displayName: this.config.displayName || DEFAULT_CONFIG.displayName,
|
|
54
|
+
icon: this.config.icon || DEFAULT_CONFIG.icon
|
|
55
|
+
});
|
|
56
|
+
const authProvider = {
|
|
57
|
+
validateCredentials: async () => null,
|
|
58
|
+
validateToken: async () => null,
|
|
59
|
+
getLoginUrl: async () => {
|
|
60
|
+
throw new Error("Magic link does not support direct getLoginUrl — POST email to /addon/auth-magic-link/request");
|
|
61
|
+
},
|
|
62
|
+
handleCallback: async () => {
|
|
63
|
+
throw new Error("Magic link does not use callback — the email link redirects to /api/auth/sso/finish directly");
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const routes = [{
|
|
67
|
+
method: "POST",
|
|
68
|
+
path: "/request",
|
|
69
|
+
access: "public",
|
|
70
|
+
description: "Request a magic link be delivered to the given email",
|
|
71
|
+
handler: async (req, reply) => this.handleRequest(req, reply)
|
|
72
|
+
}, {
|
|
73
|
+
method: "GET",
|
|
74
|
+
path: "/login",
|
|
75
|
+
access: "public",
|
|
76
|
+
description: "Click target embedded in the magic-link email",
|
|
77
|
+
handler: async (req, reply) => this.handleLogin(req, reply)
|
|
78
|
+
}];
|
|
79
|
+
const routeProvider = {
|
|
80
|
+
id: "auth-magic-link",
|
|
81
|
+
getRoutes: () => routes
|
|
82
|
+
};
|
|
83
|
+
this.ctx.logger.info("Magic-link auth provider initialized", { meta: { deliveryMode: this.config.deliveryMode } });
|
|
84
|
+
return [{
|
|
85
|
+
capability: require_dist.authProviderCapability,
|
|
86
|
+
provider: authProvider
|
|
87
|
+
}, {
|
|
88
|
+
capability: require_dist.addonRoutesCapability,
|
|
89
|
+
provider: routeProvider
|
|
90
|
+
}];
|
|
91
|
+
}
|
|
92
|
+
globalSettingsSchema() {
|
|
93
|
+
return this.schema({ sections: [{
|
|
94
|
+
id: "magic-link",
|
|
95
|
+
title: "Magic Link Settings",
|
|
96
|
+
description: "Passwordless email login. The user types an email; the hub mails them a one-time link valid for the configured TTL.",
|
|
97
|
+
columns: 1,
|
|
98
|
+
fields: [
|
|
99
|
+
this.field({
|
|
100
|
+
type: "text",
|
|
101
|
+
key: "displayName",
|
|
102
|
+
label: "Display name",
|
|
103
|
+
default: DEFAULT_CONFIG.displayName
|
|
104
|
+
}),
|
|
105
|
+
this.field({
|
|
106
|
+
type: "text",
|
|
107
|
+
key: "icon",
|
|
108
|
+
label: "Icon",
|
|
109
|
+
default: DEFAULT_CONFIG.icon
|
|
110
|
+
}),
|
|
111
|
+
this.field({
|
|
112
|
+
type: "text",
|
|
113
|
+
key: "publicOrigin",
|
|
114
|
+
label: "Public origin",
|
|
115
|
+
description: "Where the email link points. Empty = read CAMSTACK_PUBLIC_ORIGIN env var.",
|
|
116
|
+
default: ""
|
|
117
|
+
}),
|
|
118
|
+
this.field({
|
|
119
|
+
type: "select",
|
|
120
|
+
key: "deliveryMode",
|
|
121
|
+
label: "Delivery mode",
|
|
122
|
+
description: "log-only writes the URL to the addon log (setup only). smtp uses the configured SMTP relay.",
|
|
123
|
+
default: DEFAULT_CONFIG.deliveryMode,
|
|
124
|
+
options: [{
|
|
125
|
+
value: "log-only",
|
|
126
|
+
label: "Log-only (no email sent)"
|
|
127
|
+
}, {
|
|
128
|
+
value: "smtp",
|
|
129
|
+
label: "SMTP (when configured)"
|
|
130
|
+
}]
|
|
131
|
+
}),
|
|
132
|
+
this.field({
|
|
133
|
+
type: "select",
|
|
134
|
+
key: "defaultRole",
|
|
135
|
+
label: "Default role",
|
|
136
|
+
default: DEFAULT_CONFIG.defaultRole,
|
|
137
|
+
options: [{
|
|
138
|
+
value: "viewer",
|
|
139
|
+
label: "Viewer"
|
|
140
|
+
}, {
|
|
141
|
+
value: "admin",
|
|
142
|
+
label: "Admin"
|
|
143
|
+
}]
|
|
144
|
+
}),
|
|
145
|
+
this.field({
|
|
146
|
+
type: "number",
|
|
147
|
+
key: "ttlSec",
|
|
148
|
+
label: "Link TTL (seconds)",
|
|
149
|
+
description: "How long the magic link stays valid. Default 600 (10 min).",
|
|
150
|
+
default: DEFAULT_CONFIG.ttlSec
|
|
151
|
+
})
|
|
152
|
+
]
|
|
153
|
+
}] });
|
|
154
|
+
}
|
|
155
|
+
buildPublicOrigin() {
|
|
156
|
+
const configured = this.config.publicOrigin?.trim();
|
|
157
|
+
if (configured) return configured.replace(/\/+$/, "");
|
|
158
|
+
return (process.env["CAMSTACK_PUBLIC_ORIGIN"] || "").replace(/\/+$/, "") || "https://localhost:4443";
|
|
159
|
+
}
|
|
160
|
+
async findUserByEmail(email) {
|
|
161
|
+
let users;
|
|
162
|
+
try {
|
|
163
|
+
users = await this.ctx.api.userManagement.listUsers.query();
|
|
164
|
+
} catch (err) {
|
|
165
|
+
this.ctx.logger.warn("Magic-link user lookup failed (user-management cap unavailable)", { meta: { message: require_dist.errMsg(err) } });
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const needle = email.trim().toLowerCase();
|
|
169
|
+
for (const u of users) if (u.username.toLowerCase() === needle) return u;
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
async handleRequest(req, reply) {
|
|
173
|
+
try {
|
|
174
|
+
const body = req.body ?? {};
|
|
175
|
+
const email = typeof body.email === "string" ? body.email.trim() : "";
|
|
176
|
+
if (!email || !email.includes("@")) {
|
|
177
|
+
reply.code(400);
|
|
178
|
+
reply.send({ error: "A valid email address is required" });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const ok = { sent: true };
|
|
182
|
+
const user = await this.findUserByEmail(email);
|
|
183
|
+
if (!user) {
|
|
184
|
+
reply.code(200);
|
|
185
|
+
reply.send(ok);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
let token;
|
|
189
|
+
try {
|
|
190
|
+
({token} = await this.ctx.api.ssoBridge.signBridgeToken.query({
|
|
191
|
+
claims: {
|
|
192
|
+
userId: user.id,
|
|
193
|
+
username: user.username,
|
|
194
|
+
isAdmin: user.isAdmin,
|
|
195
|
+
provider: "auth-magic-link",
|
|
196
|
+
email
|
|
197
|
+
},
|
|
198
|
+
ttlSec: this.config.ttlSec || DEFAULT_CONFIG.ttlSec
|
|
199
|
+
}));
|
|
200
|
+
} catch (err) {
|
|
201
|
+
this.ctx.logger.error("Magic-link bridge-token minting failed (sso-bridge unavailable)", { meta: { message: require_dist.errMsg(err) } });
|
|
202
|
+
reply.code(503);
|
|
203
|
+
reply.send({ error: "sso-bridge capability unavailable" });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const link = `${this.buildPublicOrigin()}/addon/auth-magic-link/login?bridge=${encodeURIComponent(token)}`;
|
|
207
|
+
if (this.config.deliveryMode === "log-only") this.ctx.logger.warn("Magic link generated (log-only mode — NOT delivered via email)", { meta: {
|
|
208
|
+
email,
|
|
209
|
+
link,
|
|
210
|
+
ttlSec: this.config.ttlSec
|
|
211
|
+
} });
|
|
212
|
+
else this.ctx.logger.warn("Magic link delivery: SMTP mode requested but no smtp-provider cap registered. Falling back to log.", { meta: {
|
|
213
|
+
email,
|
|
214
|
+
link
|
|
215
|
+
} });
|
|
216
|
+
reply.code(200);
|
|
217
|
+
reply.send(ok);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
reply.code(500);
|
|
220
|
+
reply.send({
|
|
221
|
+
error: "Magic-link request failed",
|
|
222
|
+
message: require_dist.errMsg(err)
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async handleLogin(req, reply) {
|
|
227
|
+
const bridge = req.query["bridge"];
|
|
228
|
+
if (!bridge) {
|
|
229
|
+
reply.code(400);
|
|
230
|
+
reply.send({ error: "Missing bridge token" });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
reply.code(302);
|
|
234
|
+
reply.header("Location", `/api/auth/sso/finish?bridge=${encodeURIComponent(bridge)}`);
|
|
235
|
+
reply.send("");
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
//#endregion
|
|
239
|
+
exports.AuthMagicLinkAddon = AuthMagicLinkAddon;
|
|
240
|
+
exports.default = AuthMagicLinkAddon;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { a as BaseAddon, i as errMsg, n as authProviderCapability, t as addonRoutesCapability } from "../dist-JmuFs-U5.mjs";
|
|
2
|
+
//#region src/magic-link/auth-magic-link.addon.ts
|
|
3
|
+
/**
|
|
4
|
+
* Magic-link authentication addon.
|
|
5
|
+
*
|
|
6
|
+
* Passwordless flow:
|
|
7
|
+
* 1. Operator types their email on the login page → clicks "Send link".
|
|
8
|
+
* 2. The addon's `/request` route accepts the email, looks up the user
|
|
9
|
+
* via `user-management.listUsers()`, mints a short-lived HMAC bridge
|
|
10
|
+
* token (via `sso-bridge` cap), and delivers a URL of the form
|
|
11
|
+
* `https://<hub>/addon/auth-magic-link/login?bridge=<token>` to the
|
|
12
|
+
* user's email.
|
|
13
|
+
* 3. Email click hits `/login?bridge=…` → 302 to `/api/auth/sso/finish?bridge=…`
|
|
14
|
+
* → hub verifies the bridge token and mints the real session.
|
|
15
|
+
*
|
|
16
|
+
* Delivery is pluggable. This addon ships a "log-only" mode by default
|
|
17
|
+
* (writes the URL to the addon log so operators can copy it during
|
|
18
|
+
* setup before SMTP is configured). Production deployments configure
|
|
19
|
+
* the SMTP relay via global settings.
|
|
20
|
+
*
|
|
21
|
+
* Open follow-ups:
|
|
22
|
+
* - Wire `nodemailer` for real SMTP delivery (peer dep; the framework
|
|
23
|
+
* declares `smtpProvider` cap if/when we ship an SMTP addon).
|
|
24
|
+
* - Throttle requests per-email (rate-limit window).
|
|
25
|
+
* - Auto-provision unknown emails (currently rejected for safety).
|
|
26
|
+
*/
|
|
27
|
+
var DEFAULT_CONFIG = {
|
|
28
|
+
displayName: "Email magic link",
|
|
29
|
+
icon: "mail",
|
|
30
|
+
publicOrigin: "",
|
|
31
|
+
deliveryMode: "log-only",
|
|
32
|
+
defaultRole: "viewer",
|
|
33
|
+
ttlSec: 600
|
|
34
|
+
};
|
|
35
|
+
var AuthMagicLinkAddon = class extends BaseAddon {
|
|
36
|
+
kind = "magic-link";
|
|
37
|
+
hasRedirectFlow = false;
|
|
38
|
+
hasCredentialFlow = true;
|
|
39
|
+
displayName;
|
|
40
|
+
icon;
|
|
41
|
+
constructor() {
|
|
42
|
+
super({ ...DEFAULT_CONFIG });
|
|
43
|
+
this.displayName = DEFAULT_CONFIG.displayName;
|
|
44
|
+
this.icon = DEFAULT_CONFIG.icon;
|
|
45
|
+
}
|
|
46
|
+
async onInitialize() {
|
|
47
|
+
Object.assign(this, {
|
|
48
|
+
displayName: this.config.displayName || DEFAULT_CONFIG.displayName,
|
|
49
|
+
icon: this.config.icon || DEFAULT_CONFIG.icon
|
|
50
|
+
});
|
|
51
|
+
const authProvider = {
|
|
52
|
+
validateCredentials: async () => null,
|
|
53
|
+
validateToken: async () => null,
|
|
54
|
+
getLoginUrl: async () => {
|
|
55
|
+
throw new Error("Magic link does not support direct getLoginUrl — POST email to /addon/auth-magic-link/request");
|
|
56
|
+
},
|
|
57
|
+
handleCallback: async () => {
|
|
58
|
+
throw new Error("Magic link does not use callback — the email link redirects to /api/auth/sso/finish directly");
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const routes = [{
|
|
62
|
+
method: "POST",
|
|
63
|
+
path: "/request",
|
|
64
|
+
access: "public",
|
|
65
|
+
description: "Request a magic link be delivered to the given email",
|
|
66
|
+
handler: async (req, reply) => this.handleRequest(req, reply)
|
|
67
|
+
}, {
|
|
68
|
+
method: "GET",
|
|
69
|
+
path: "/login",
|
|
70
|
+
access: "public",
|
|
71
|
+
description: "Click target embedded in the magic-link email",
|
|
72
|
+
handler: async (req, reply) => this.handleLogin(req, reply)
|
|
73
|
+
}];
|
|
74
|
+
const routeProvider = {
|
|
75
|
+
id: "auth-magic-link",
|
|
76
|
+
getRoutes: () => routes
|
|
77
|
+
};
|
|
78
|
+
this.ctx.logger.info("Magic-link auth provider initialized", { meta: { deliveryMode: this.config.deliveryMode } });
|
|
79
|
+
return [{
|
|
80
|
+
capability: authProviderCapability,
|
|
81
|
+
provider: authProvider
|
|
82
|
+
}, {
|
|
83
|
+
capability: addonRoutesCapability,
|
|
84
|
+
provider: routeProvider
|
|
85
|
+
}];
|
|
86
|
+
}
|
|
87
|
+
globalSettingsSchema() {
|
|
88
|
+
return this.schema({ sections: [{
|
|
89
|
+
id: "magic-link",
|
|
90
|
+
title: "Magic Link Settings",
|
|
91
|
+
description: "Passwordless email login. The user types an email; the hub mails them a one-time link valid for the configured TTL.",
|
|
92
|
+
columns: 1,
|
|
93
|
+
fields: [
|
|
94
|
+
this.field({
|
|
95
|
+
type: "text",
|
|
96
|
+
key: "displayName",
|
|
97
|
+
label: "Display name",
|
|
98
|
+
default: DEFAULT_CONFIG.displayName
|
|
99
|
+
}),
|
|
100
|
+
this.field({
|
|
101
|
+
type: "text",
|
|
102
|
+
key: "icon",
|
|
103
|
+
label: "Icon",
|
|
104
|
+
default: DEFAULT_CONFIG.icon
|
|
105
|
+
}),
|
|
106
|
+
this.field({
|
|
107
|
+
type: "text",
|
|
108
|
+
key: "publicOrigin",
|
|
109
|
+
label: "Public origin",
|
|
110
|
+
description: "Where the email link points. Empty = read CAMSTACK_PUBLIC_ORIGIN env var.",
|
|
111
|
+
default: ""
|
|
112
|
+
}),
|
|
113
|
+
this.field({
|
|
114
|
+
type: "select",
|
|
115
|
+
key: "deliveryMode",
|
|
116
|
+
label: "Delivery mode",
|
|
117
|
+
description: "log-only writes the URL to the addon log (setup only). smtp uses the configured SMTP relay.",
|
|
118
|
+
default: DEFAULT_CONFIG.deliveryMode,
|
|
119
|
+
options: [{
|
|
120
|
+
value: "log-only",
|
|
121
|
+
label: "Log-only (no email sent)"
|
|
122
|
+
}, {
|
|
123
|
+
value: "smtp",
|
|
124
|
+
label: "SMTP (when configured)"
|
|
125
|
+
}]
|
|
126
|
+
}),
|
|
127
|
+
this.field({
|
|
128
|
+
type: "select",
|
|
129
|
+
key: "defaultRole",
|
|
130
|
+
label: "Default role",
|
|
131
|
+
default: DEFAULT_CONFIG.defaultRole,
|
|
132
|
+
options: [{
|
|
133
|
+
value: "viewer",
|
|
134
|
+
label: "Viewer"
|
|
135
|
+
}, {
|
|
136
|
+
value: "admin",
|
|
137
|
+
label: "Admin"
|
|
138
|
+
}]
|
|
139
|
+
}),
|
|
140
|
+
this.field({
|
|
141
|
+
type: "number",
|
|
142
|
+
key: "ttlSec",
|
|
143
|
+
label: "Link TTL (seconds)",
|
|
144
|
+
description: "How long the magic link stays valid. Default 600 (10 min).",
|
|
145
|
+
default: DEFAULT_CONFIG.ttlSec
|
|
146
|
+
})
|
|
147
|
+
]
|
|
148
|
+
}] });
|
|
149
|
+
}
|
|
150
|
+
buildPublicOrigin() {
|
|
151
|
+
const configured = this.config.publicOrigin?.trim();
|
|
152
|
+
if (configured) return configured.replace(/\/+$/, "");
|
|
153
|
+
return (process.env["CAMSTACK_PUBLIC_ORIGIN"] || "").replace(/\/+$/, "") || "https://localhost:4443";
|
|
154
|
+
}
|
|
155
|
+
async findUserByEmail(email) {
|
|
156
|
+
let users;
|
|
157
|
+
try {
|
|
158
|
+
users = await this.ctx.api.userManagement.listUsers.query();
|
|
159
|
+
} catch (err) {
|
|
160
|
+
this.ctx.logger.warn("Magic-link user lookup failed (user-management cap unavailable)", { meta: { message: errMsg(err) } });
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const needle = email.trim().toLowerCase();
|
|
164
|
+
for (const u of users) if (u.username.toLowerCase() === needle) return u;
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
async handleRequest(req, reply) {
|
|
168
|
+
try {
|
|
169
|
+
const body = req.body ?? {};
|
|
170
|
+
const email = typeof body.email === "string" ? body.email.trim() : "";
|
|
171
|
+
if (!email || !email.includes("@")) {
|
|
172
|
+
reply.code(400);
|
|
173
|
+
reply.send({ error: "A valid email address is required" });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const ok = { sent: true };
|
|
177
|
+
const user = await this.findUserByEmail(email);
|
|
178
|
+
if (!user) {
|
|
179
|
+
reply.code(200);
|
|
180
|
+
reply.send(ok);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
let token;
|
|
184
|
+
try {
|
|
185
|
+
({token} = await this.ctx.api.ssoBridge.signBridgeToken.query({
|
|
186
|
+
claims: {
|
|
187
|
+
userId: user.id,
|
|
188
|
+
username: user.username,
|
|
189
|
+
isAdmin: user.isAdmin,
|
|
190
|
+
provider: "auth-magic-link",
|
|
191
|
+
email
|
|
192
|
+
},
|
|
193
|
+
ttlSec: this.config.ttlSec || DEFAULT_CONFIG.ttlSec
|
|
194
|
+
}));
|
|
195
|
+
} catch (err) {
|
|
196
|
+
this.ctx.logger.error("Magic-link bridge-token minting failed (sso-bridge unavailable)", { meta: { message: errMsg(err) } });
|
|
197
|
+
reply.code(503);
|
|
198
|
+
reply.send({ error: "sso-bridge capability unavailable" });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const link = `${this.buildPublicOrigin()}/addon/auth-magic-link/login?bridge=${encodeURIComponent(token)}`;
|
|
202
|
+
if (this.config.deliveryMode === "log-only") this.ctx.logger.warn("Magic link generated (log-only mode — NOT delivered via email)", { meta: {
|
|
203
|
+
email,
|
|
204
|
+
link,
|
|
205
|
+
ttlSec: this.config.ttlSec
|
|
206
|
+
} });
|
|
207
|
+
else this.ctx.logger.warn("Magic link delivery: SMTP mode requested but no smtp-provider cap registered. Falling back to log.", { meta: {
|
|
208
|
+
email,
|
|
209
|
+
link
|
|
210
|
+
} });
|
|
211
|
+
reply.code(200);
|
|
212
|
+
reply.send(ok);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
reply.code(500);
|
|
215
|
+
reply.send({
|
|
216
|
+
error: "Magic-link request failed",
|
|
217
|
+
message: errMsg(err)
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async handleLogin(req, reply) {
|
|
222
|
+
const bridge = req.query["bridge"];
|
|
223
|
+
if (!bridge) {
|
|
224
|
+
reply.code(400);
|
|
225
|
+
reply.send({ error: "Missing bridge token" });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
reply.code(302);
|
|
229
|
+
reply.header("Location", `/api/auth/sso/finish?bridge=${encodeURIComponent(bridge)}`);
|
|
230
|
+
reply.send("");
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
//#endregion
|
|
234
|
+
export { AuthMagicLinkAddon, AuthMagicLinkAddon as default };
|