@miguelmorales13/nestkit 0.5.1 → 0.7.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/README.md +250 -6
- package/dist/auth/index.cjs +12 -70
- package/dist/auth/index.js +11 -69
- package/dist/auth/oauth/http.d.ts +38 -0
- package/dist/auth/oauth/index.cjs +610 -0
- package/dist/auth/oauth/index.d.ts +12 -0
- package/dist/auth/oauth/index.js +610 -0
- package/dist/auth/oauth/oauth-auth.module.d.ts +28 -0
- package/dist/auth/oauth/oauth-auth.service.d.ts +101 -0
- package/dist/auth/oauth/oauth-callback.filter.d.ts +20 -0
- package/dist/auth/oauth/oauth-provider.d.ts +73 -0
- package/dist/auth/oauth/oauth.controller.d.ts +38 -0
- package/dist/auth/oauth/oauth.options.d.ts +89 -0
- package/dist/auth/oauth/oauth.ports.d.ts +86 -0
- package/dist/bootstrap/index.cjs +3 -3
- package/dist/bootstrap/index.js +2 -2
- package/dist/chunk-A3B2EY4V.js +75 -0
- package/dist/chunk-NVCI3CQI.cjs +75 -0
- package/dist/database/typeorm/index.cjs +79 -0
- package/dist/database/typeorm/index.d.ts +3 -0
- package/dist/database/typeorm/index.js +79 -0
- package/dist/database/typeorm/tenant-scope.d.ts +18 -0
- package/dist/database/typeorm/typeorm.module.d.ts +35 -0
- package/dist/index.cjs +17 -17
- package/dist/index.js +24 -24
- package/dist/umami/index.cjs +109 -0
- package/dist/umami/index.d.ts +4 -0
- package/dist/umami/index.js +109 -0
- package/dist/umami/umami.module.d.ts +6 -0
- package/dist/umami/umami.options.d.ts +20 -0
- package/dist/umami/umami.service.d.ts +40 -0
- package/dist/umami/umami.types.d.ts +24 -0
- package/package.json +22 -2
- package/dist/{chunk-AOCF5QCZ.js → chunk-ANQ3YPDI.js} +3 -3
- package/dist/{chunk-54ZXIB5T.cjs → chunk-YARLPYG5.cjs} +2 -2
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
var _chunkNVCI3CQIcjs = require('../../chunk-NVCI3CQI.cjs');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
var _chunkFDNGAYTZcjs = require('../../chunk-FDNGAYTZ.cjs');
|
|
9
|
+
require('../../chunk-R7BVS6CI.cjs');
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
var _chunk2REOCMUDcjs = require('../../chunk-2REOCMUD.cjs');
|
|
14
|
+
|
|
15
|
+
// src/auth/oauth/oauth-provider.ts
|
|
16
|
+
async function readJson(response, what) {
|
|
17
|
+
const text = await response.text();
|
|
18
|
+
let body;
|
|
19
|
+
try {
|
|
20
|
+
body = JSON.parse(text);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
throw new Error(`${what}: expected JSON, got ${response.status} ${text.slice(0, 200)}`);
|
|
23
|
+
}
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
throw new Error(`${what}: HTTP ${response.status} ${text.slice(0, 200)}`);
|
|
26
|
+
}
|
|
27
|
+
return body;
|
|
28
|
+
}
|
|
29
|
+
function googleProvider(credentials = {}) {
|
|
30
|
+
return {
|
|
31
|
+
name: "google",
|
|
32
|
+
clientId: _nullishCoalesce(credentials.clientId, () => ( process.env.GOOGLE_CLIENT_ID)),
|
|
33
|
+
clientSecret: _nullishCoalesce(credentials.clientSecret, () => ( process.env.GOOGLE_CLIENT_SECRET)),
|
|
34
|
+
callbackUrl: _nullishCoalesce(credentials.callbackUrl, () => ( process.env.GOOGLE_CALLBACK_URL)),
|
|
35
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
36
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
37
|
+
scope: _nullishCoalesce(credentials.scope, () => ( "email profile")),
|
|
38
|
+
authorizeParams: {
|
|
39
|
+
// Without this, someone signed into several Google accounts is put
|
|
40
|
+
// straight back into the one the browser last used, with no way to pick
|
|
41
|
+
// — which reads as the app ignoring who they wanted to be.
|
|
42
|
+
prompt: "select_account",
|
|
43
|
+
access_type: "offline"
|
|
44
|
+
},
|
|
45
|
+
async fetchProfile(accessToken) {
|
|
46
|
+
const data = await readJson(
|
|
47
|
+
await fetch("https://www.googleapis.com/oauth2/v3/userinfo", {
|
|
48
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
49
|
+
}),
|
|
50
|
+
"Google userinfo"
|
|
51
|
+
);
|
|
52
|
+
const email = data.email;
|
|
53
|
+
if (!email) throw new Error("Google did not return an email.");
|
|
54
|
+
return {
|
|
55
|
+
providerAccountId: String(data.sub),
|
|
56
|
+
email,
|
|
57
|
+
name: data.name,
|
|
58
|
+
avatarUrl: data.picture
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function facebookProvider(credentials = {}) {
|
|
64
|
+
const version = _nullishCoalesce(process.env.FACEBOOK_GRAPH_VERSION, () => ( "v21.0"));
|
|
65
|
+
return {
|
|
66
|
+
name: "facebook",
|
|
67
|
+
clientId: _nullishCoalesce(credentials.clientId, () => ( process.env.FACEBOOK_APP_ID)),
|
|
68
|
+
clientSecret: _nullishCoalesce(credentials.clientSecret, () => ( process.env.FACEBOOK_APP_SECRET)),
|
|
69
|
+
callbackUrl: _nullishCoalesce(credentials.callbackUrl, () => ( process.env.FACEBOOK_CALLBACK_URL)),
|
|
70
|
+
authorizeUrl: `https://www.facebook.com/${version}/dialog/oauth`,
|
|
71
|
+
tokenUrl: `https://graph.facebook.com/${version}/oauth/access_token`,
|
|
72
|
+
scope: _nullishCoalesce(credentials.scope, () => ( "email")),
|
|
73
|
+
async fetchProfile(accessToken) {
|
|
74
|
+
const url = new URL(`https://graph.facebook.com/${version}/me`);
|
|
75
|
+
url.searchParams.set("fields", "id,name,email,picture.type(large)");
|
|
76
|
+
const data = await readJson(
|
|
77
|
+
await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } }),
|
|
78
|
+
"Facebook me"
|
|
79
|
+
);
|
|
80
|
+
const picture = data.picture;
|
|
81
|
+
return {
|
|
82
|
+
providerAccountId: String(data.id),
|
|
83
|
+
email: _nullishCoalesce(data.email, () => ( "")),
|
|
84
|
+
name: data.name,
|
|
85
|
+
avatarUrl: _optionalChain([picture, 'optionalAccess', _ => _.data, 'optionalAccess', _2 => _2.url])
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function buildAuthorizeUrl(provider, state) {
|
|
91
|
+
const params = new URLSearchParams({
|
|
92
|
+
client_id: _nullishCoalesce(provider.clientId, () => ( "")),
|
|
93
|
+
redirect_uri: _nullishCoalesce(provider.callbackUrl, () => ( "")),
|
|
94
|
+
response_type: "code",
|
|
95
|
+
scope: provider.scope,
|
|
96
|
+
...provider.authorizeParams,
|
|
97
|
+
...state ? { state } : {}
|
|
98
|
+
});
|
|
99
|
+
return `${provider.authorizeUrl}?${params.toString()}`;
|
|
100
|
+
}
|
|
101
|
+
async function exchangeCodeForToken(provider, code) {
|
|
102
|
+
const body = new URLSearchParams({
|
|
103
|
+
client_id: _nullishCoalesce(provider.clientId, () => ( "")),
|
|
104
|
+
client_secret: _nullishCoalesce(provider.clientSecret, () => ( "")),
|
|
105
|
+
redirect_uri: _nullishCoalesce(provider.callbackUrl, () => ( "")),
|
|
106
|
+
grant_type: "authorization_code",
|
|
107
|
+
code
|
|
108
|
+
});
|
|
109
|
+
const data = await readJson(
|
|
110
|
+
await fetch(provider.tokenUrl, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: {
|
|
113
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
114
|
+
Accept: "application/json"
|
|
115
|
+
},
|
|
116
|
+
body
|
|
117
|
+
}),
|
|
118
|
+
`${provider.name} token exchange`
|
|
119
|
+
);
|
|
120
|
+
const token = data.access_token;
|
|
121
|
+
if (!token) throw new Error(`${provider.name} token exchange returned no access_token.`);
|
|
122
|
+
return token;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/auth/oauth/oauth.options.ts
|
|
126
|
+
var OAUTH_AUTH_OPTIONS = /* @__PURE__ */ Symbol("OAUTH_AUTH_OPTIONS");
|
|
127
|
+
var OAUTH_STORE = /* @__PURE__ */ Symbol("OAUTH_STORE");
|
|
128
|
+
var REFRESH_TOKEN_STORE = /* @__PURE__ */ Symbol("REFRESH_TOKEN_STORE");
|
|
129
|
+
function resolveOAuthOptions(options) {
|
|
130
|
+
const webUrl = options.webUrl.replace(/\/$/, "");
|
|
131
|
+
return {
|
|
132
|
+
providers: options.providers,
|
|
133
|
+
webUrl,
|
|
134
|
+
onLoginRedirect: _nullishCoalesce(options.onLoginRedirect, () => ( ((accessToken) => `${webUrl}/oauth-callback#accessToken=${accessToken}`))),
|
|
135
|
+
onErrorRedirect: _nullishCoalesce(options.onErrorRedirect, () => ( ((message) => `${webUrl}/login?error=${encodeURIComponent(message)}`))),
|
|
136
|
+
onLinkRedirect: _nullishCoalesce(options.onLinkRedirect, () => ( ((provider, error) => error ? `${webUrl}/profile?error=${encodeURIComponent(error)}` : `${webUrl}/profile?linked=${provider}`))),
|
|
137
|
+
cookie: {
|
|
138
|
+
name: _nullishCoalesce(_optionalChain([options, 'access', _3 => _3.cookie, 'optionalAccess', _4 => _4.name]), () => ( "refresh_token")),
|
|
139
|
+
path: _nullishCoalesce(_optionalChain([options, 'access', _5 => _5.cookie, 'optionalAccess', _6 => _6.path]), () => ( "/auth")),
|
|
140
|
+
sameSite: _nullishCoalesce(_optionalChain([options, 'access', _7 => _7.cookie, 'optionalAccess', _8 => _8.sameSite]), () => ( "lax")),
|
|
141
|
+
secure: _nullishCoalesce(_optionalChain([options, 'access', _9 => _9.cookie, 'optionalAccess', _10 => _10.secure]), () => ( process.env.NODE_ENV === "production"))
|
|
142
|
+
},
|
|
143
|
+
linkCookieName: _nullishCoalesce(options.linkCookieName, () => ( "link_intent")),
|
|
144
|
+
accessTtlSeconds: _nullishCoalesce(options.accessTtlSeconds, () => ( 15 * 60)),
|
|
145
|
+
refreshTtlSeconds: _nullishCoalesce(options.refreshTtlSeconds, () => ( 30 * 24 * 60 * 60)),
|
|
146
|
+
linkTtlSeconds: _nullishCoalesce(options.linkTtlSeconds, () => ( 10 * 60)),
|
|
147
|
+
accessSecret: options.accessSecret,
|
|
148
|
+
refreshSecret: options.refreshSecret,
|
|
149
|
+
singleSession: _nullishCoalesce(options.singleSession, () => ( true)),
|
|
150
|
+
syncProfile: _nullishCoalesce(options.syncProfile, () => ( ((_user, profile) => ({ name: profile.name, avatarUrl: _nullishCoalesce(profile.avatarUrl, () => ( null)) })))),
|
|
151
|
+
mapUser: options.mapUser,
|
|
152
|
+
unavailableMessage: _nullishCoalesce(options.unavailableMessage, () => ( ((provider) => `Signing in with ${provider} is unavailable.`))),
|
|
153
|
+
genericErrorMessage: _nullishCoalesce(options.genericErrorMessage, () => ( (() => "Sign-in could not be completed. Please try again."))),
|
|
154
|
+
missingEmailMessage: _nullishCoalesce(options.missingEmailMessage, () => ( ((provider) => `${provider} did not share your email address, so we cannot create your account.`)))
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/auth/oauth/oauth-auth.service.ts
|
|
159
|
+
var _common = require('@nestjs/common');
|
|
160
|
+
var _crypto = require('crypto');
|
|
161
|
+
var _jsonwebtoken = require('jsonwebtoken'); var _jsonwebtoken2 = _interopRequireDefault(_jsonwebtoken);
|
|
162
|
+
function hashToken(token) {
|
|
163
|
+
return _crypto.createHash.call(void 0, "sha256").update(token).digest("hex");
|
|
164
|
+
}
|
|
165
|
+
function requireSecret(explicit, envName) {
|
|
166
|
+
const value = _nullishCoalesce(explicit, () => ( process.env[envName]));
|
|
167
|
+
if (!value) throw new Error(`OAuthAuthService: ${envName} is not set`);
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
var OAuthAuthService = class {
|
|
171
|
+
constructor(options, store, sessions) {
|
|
172
|
+
this.store = store;
|
|
173
|
+
this.sessions = sessions;
|
|
174
|
+
this.options = resolveOAuthOptions(options);
|
|
175
|
+
}
|
|
176
|
+
get config() {
|
|
177
|
+
return this.options;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Looks up a configured provider, or explains that it is unavailable.
|
|
181
|
+
*
|
|
182
|
+
* Missing credentials make one provider stop existing rather than stopping
|
|
183
|
+
* the boot — the same fail-late rule the rest of nestkit's optional
|
|
184
|
+
* integrations follow. An unknown name and an unconfigured one answer
|
|
185
|
+
* identically on purpose: neither is something the caller can act on
|
|
186
|
+
* differently, and distinguishing them only tells a prober what exists.
|
|
187
|
+
*/
|
|
188
|
+
provider(name) {
|
|
189
|
+
const found = this.options.providers.find((p) => p.name === name);
|
|
190
|
+
if (!_optionalChain([found, 'optionalAccess', _11 => _11.clientId]) || !found.clientSecret) {
|
|
191
|
+
throw new (0, _common.ServiceUnavailableException)(this.options.unavailableMessage(name));
|
|
192
|
+
}
|
|
193
|
+
return found;
|
|
194
|
+
}
|
|
195
|
+
authorizeUrl(name) {
|
|
196
|
+
return buildAuthorizeUrl(this.provider(name));
|
|
197
|
+
}
|
|
198
|
+
/** Exchanges the callback's `code` for a normalised profile. */
|
|
199
|
+
async profileFromCode(name, code) {
|
|
200
|
+
const provider = this.provider(name);
|
|
201
|
+
const accessToken = await exchangeCodeForToken(provider, code);
|
|
202
|
+
const profile = await provider.fetchProfile(accessToken);
|
|
203
|
+
if (!profile.email) {
|
|
204
|
+
throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)(this.options.missingEmailMessage(name));
|
|
205
|
+
}
|
|
206
|
+
return { ...profile, email: normaliseEmail(profile.email) };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Signs someone in, creating the account the first time.
|
|
210
|
+
*
|
|
211
|
+
* The provider is a parameter because the flow is identical for all of
|
|
212
|
+
* them: find the linked account, and failing that link by email to whoever
|
|
213
|
+
* is already registered. That email link is what makes signing in with
|
|
214
|
+
* Facebook today and Google tomorrow land on one account instead of two.
|
|
215
|
+
*/
|
|
216
|
+
async login(providerName, profile) {
|
|
217
|
+
const provider = this.provider(providerName).name;
|
|
218
|
+
const existing = await this.store.findAccount(provider, profile.providerAccountId);
|
|
219
|
+
let user = _nullishCoalesce(_optionalChain([existing, 'optionalAccess', _12 => _12.user]), () => ( null));
|
|
220
|
+
if (!user) {
|
|
221
|
+
const email = normaliseEmail(profile.email);
|
|
222
|
+
user = await _asyncNullishCoalesce(await this.store.findUserByEmail(email), async () => ( await this.store.createUser({
|
|
223
|
+
email,
|
|
224
|
+
name: profile.name,
|
|
225
|
+
avatarUrl: profile.avatarUrl
|
|
226
|
+
})));
|
|
227
|
+
await this.store.createAccount({
|
|
228
|
+
userId: user.id,
|
|
229
|
+
provider,
|
|
230
|
+
providerAccountId: profile.providerAccountId,
|
|
231
|
+
avatarUrl: _nullishCoalesce(profile.avatarUrl, () => ( null))
|
|
232
|
+
});
|
|
233
|
+
} else {
|
|
234
|
+
await this.store.updateAccountAvatar(
|
|
235
|
+
provider,
|
|
236
|
+
profile.providerAccountId,
|
|
237
|
+
_nullishCoalesce(profile.avatarUrl, () => ( null))
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const changes = this.options.syncProfile(user, profile);
|
|
241
|
+
if (changes && (changes.name !== user.name || changes.avatarUrl !== user.avatarUrl)) {
|
|
242
|
+
user = await this.store.updateUser(user.id, changes);
|
|
243
|
+
}
|
|
244
|
+
if (this.options.singleSession) {
|
|
245
|
+
await this.sessions.revokeAllForUser(user.id);
|
|
246
|
+
}
|
|
247
|
+
const tokens = await this.issueTokenPair(user);
|
|
248
|
+
return { user: this.present(user), ...tokens };
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Rotates a refresh token: the old one is spent, a new pair comes back.
|
|
252
|
+
*
|
|
253
|
+
* Revoking before issuing, and only continuing when the revoke actually hit
|
|
254
|
+
* a live row, is the whole point — replaying a token that has already been
|
|
255
|
+
* used finds nothing to revoke and is rejected.
|
|
256
|
+
*/
|
|
257
|
+
async refresh(rawRefreshToken) {
|
|
258
|
+
const payload = this.verifyRefresh(rawRefreshToken);
|
|
259
|
+
const { count } = await this.sessions.revokeIfActive(hashToken(rawRefreshToken));
|
|
260
|
+
if (count === 0) throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)("Invalid refresh token.");
|
|
261
|
+
const user = await this.store.findUserById(payload.sub);
|
|
262
|
+
if (!user) throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)("Invalid refresh token.");
|
|
263
|
+
return this.issueTokenPair(user);
|
|
264
|
+
}
|
|
265
|
+
async logout(rawRefreshToken) {
|
|
266
|
+
await this.sessions.revokeIfActive(hashToken(rawRefreshToken));
|
|
267
|
+
}
|
|
268
|
+
async currentUser(userId) {
|
|
269
|
+
const user = await this.store.findUserById(userId);
|
|
270
|
+
if (!user) throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)("User not found.");
|
|
271
|
+
return this.present(user);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Issues the short-lived permission to link another provider.
|
|
275
|
+
*
|
|
276
|
+
* Needed because the OAuth leg is a whole-browser navigation, not a request
|
|
277
|
+
* with headers: the access token does not travel there. So a permission is
|
|
278
|
+
* minted, parked in an httpOnly cookie, and read back by the callback to
|
|
279
|
+
* tell "link this to me" from "sign in". It carries its own `typ` so it can
|
|
280
|
+
* never pass as an access token, or the other way round, despite sharing a
|
|
281
|
+
* secret.
|
|
282
|
+
*/
|
|
283
|
+
issueLinkToken(userId) {
|
|
284
|
+
const ttl = this.options.linkTtlSeconds;
|
|
285
|
+
const token = _jsonwebtoken2.default.sign(
|
|
286
|
+
{ sub: userId, typ: "link" },
|
|
287
|
+
requireSecret(this.options.accessSecret, "JWT_ACCESS_SECRET"),
|
|
288
|
+
{ expiresIn: ttl }
|
|
289
|
+
);
|
|
290
|
+
return { token, expiresAt: new Date(Date.now() + ttl * 1e3) };
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The user id behind a link permission, or null.
|
|
294
|
+
*
|
|
295
|
+
* Null rather than an exception: with no valid permission the callback just
|
|
296
|
+
* performs an ordinary sign-in, which is the correct outcome.
|
|
297
|
+
*/
|
|
298
|
+
readLinkToken(token) {
|
|
299
|
+
if (!token) return null;
|
|
300
|
+
try {
|
|
301
|
+
const payload = _jsonwebtoken2.default.verify(
|
|
302
|
+
token,
|
|
303
|
+
requireSecret(this.options.accessSecret, "JWT_ACCESS_SECRET")
|
|
304
|
+
);
|
|
305
|
+
return payload.typ === "link" && payload.sub ? payload.sub : null;
|
|
306
|
+
} catch (e2) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Ties a provider account to the already-signed-in user.
|
|
312
|
+
*
|
|
313
|
+
* This is the way out when the emails do not match: if someone's Facebook
|
|
314
|
+
* is registered under a different address, linking by email can never join
|
|
315
|
+
* them, and without this they keep two profiles forever.
|
|
316
|
+
*/
|
|
317
|
+
async linkAccount(userId, providerName, profile) {
|
|
318
|
+
const provider = this.provider(providerName).name;
|
|
319
|
+
const existing = await this.store.findAccount(provider, profile.providerAccountId);
|
|
320
|
+
if (existing && existing.userId !== userId) {
|
|
321
|
+
throw new (0, _chunkFDNGAYTZcjs.ValidationAppException)(
|
|
322
|
+
"That account is already connected to another profile.",
|
|
323
|
+
{ provider },
|
|
324
|
+
"OAUTH_ACCOUNT_TAKEN"
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const alreadyLinked = await this.store.findAccountByUserAndProvider(userId, provider);
|
|
328
|
+
if (alreadyLinked && alreadyLinked.providerAccountId !== profile.providerAccountId) {
|
|
329
|
+
throw new (0, _chunkFDNGAYTZcjs.ValidationAppException)(
|
|
330
|
+
"You already have an account from that provider connected.",
|
|
331
|
+
{ provider },
|
|
332
|
+
"OAUTH_PROVIDER_ALREADY_LINKED"
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (existing) {
|
|
336
|
+
await this.store.updateAccountAvatar(
|
|
337
|
+
provider,
|
|
338
|
+
profile.providerAccountId,
|
|
339
|
+
_nullishCoalesce(profile.avatarUrl, () => ( null))
|
|
340
|
+
);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
await this.store.createAccount({
|
|
344
|
+
userId,
|
|
345
|
+
provider,
|
|
346
|
+
providerAccountId: profile.providerAccountId,
|
|
347
|
+
avatarUrl: _nullishCoalesce(profile.avatarUrl, () => ( null))
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Which providers are linked, and what photo each one offers.
|
|
352
|
+
*
|
|
353
|
+
* Enough to build both a "connect another account" screen and an avatar
|
|
354
|
+
* picker; what the app does with the choice is the app's decision.
|
|
355
|
+
*/
|
|
356
|
+
async linkedAccounts(userId) {
|
|
357
|
+
const accounts = await this.store.listAccountsByUser(userId);
|
|
358
|
+
return {
|
|
359
|
+
connected: accounts.map((a) => a.provider),
|
|
360
|
+
avatars: accounts.filter((a) => Boolean(a.avatarUrl)).map((a) => ({ provider: a.provider, avatarUrl: a.avatarUrl }))
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
present(user) {
|
|
364
|
+
return this.options.mapUser ? this.options.mapUser(user) : user;
|
|
365
|
+
}
|
|
366
|
+
async issueTokenPair(user) {
|
|
367
|
+
const accessToken = _jsonwebtoken2.default.sign(
|
|
368
|
+
{ sub: user.id, email: user.email },
|
|
369
|
+
requireSecret(this.options.accessSecret, "JWT_ACCESS_SECRET"),
|
|
370
|
+
{ expiresIn: this.options.accessTtlSeconds }
|
|
371
|
+
);
|
|
372
|
+
const refreshToken = _jsonwebtoken2.default.sign(
|
|
373
|
+
// The jti keeps two tokens minted for the same user in the same second
|
|
374
|
+
// from coming out byte-identical — a JWT is deterministic given the
|
|
375
|
+
// same payload and secret, and identical tokens collide on the unique
|
|
376
|
+
// index over the stored hash.
|
|
377
|
+
{ sub: user.id, jti: _crypto.randomUUID.call(void 0, ) },
|
|
378
|
+
requireSecret(this.options.refreshSecret, "JWT_REFRESH_SECRET"),
|
|
379
|
+
{ expiresIn: this.options.refreshTtlSeconds }
|
|
380
|
+
);
|
|
381
|
+
const expiresAt = new Date(Date.now() + this.options.refreshTtlSeconds * 1e3);
|
|
382
|
+
await this.sessions.create(user.id, hashToken(refreshToken), expiresAt);
|
|
383
|
+
return { accessToken, refreshToken, refreshTokenExpiresAt: expiresAt };
|
|
384
|
+
}
|
|
385
|
+
verifyRefresh(token) {
|
|
386
|
+
try {
|
|
387
|
+
return _jsonwebtoken2.default.verify(
|
|
388
|
+
token,
|
|
389
|
+
requireSecret(this.options.refreshSecret, "JWT_REFRESH_SECRET")
|
|
390
|
+
);
|
|
391
|
+
} catch (e3) {
|
|
392
|
+
throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)("Invalid refresh token.");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
OAuthAuthService = exports.OAuthAuthService = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
397
|
+
_common.Injectable.call(void 0, ),
|
|
398
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 0, _common.Inject.call(void 0, OAUTH_AUTH_OPTIONS)),
|
|
399
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 1, _common.Inject.call(void 0, OAUTH_STORE)),
|
|
400
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 2, _common.Inject.call(void 0, REFRESH_TOKEN_STORE))
|
|
401
|
+
], OAuthAuthService);
|
|
402
|
+
function normaliseEmail(email) {
|
|
403
|
+
return email.trim().toLowerCase();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/auth/oauth/oauth-callback.filter.ts
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
// src/auth/oauth/http.ts
|
|
415
|
+
function setCookie(reply, name, value, options) {
|
|
416
|
+
if (typeof reply.setCookie === "function") reply.setCookie(name, value, options);
|
|
417
|
+
else if (typeof reply.cookie === "function") reply.cookie(name, value, options);
|
|
418
|
+
else throw new Error("No cookie support on the reply: register @fastify/cookie or cookie-parser.");
|
|
419
|
+
}
|
|
420
|
+
function clearCookie(reply, name, options) {
|
|
421
|
+
_optionalChain([reply, 'access', _13 => _13.clearCookie, 'optionalCall', _14 => _14(name, options)]);
|
|
422
|
+
}
|
|
423
|
+
function redirect(reply, url) {
|
|
424
|
+
reply.redirect(url, 302);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/auth/oauth/oauth-callback.filter.ts
|
|
428
|
+
var OAuthCallbackFilter = class {
|
|
429
|
+
constructor(options) {
|
|
430
|
+
this.logger = new (0, _common.Logger)(OAuthCallbackFilter.name);
|
|
431
|
+
this.options = resolveOAuthOptions(options);
|
|
432
|
+
}
|
|
433
|
+
catch(exception, host) {
|
|
434
|
+
const reply = host.switchToHttp().getResponse();
|
|
435
|
+
this.logger.error(
|
|
436
|
+
`OAuth callback failed: ${exception instanceof Error ? exception.stack : String(exception)}`
|
|
437
|
+
);
|
|
438
|
+
const message = exception instanceof _common.HttpException ? exception.message : this.options.genericErrorMessage();
|
|
439
|
+
redirect(reply, this.options.onErrorRedirect(message));
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
OAuthCallbackFilter = exports.OAuthCallbackFilter = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
443
|
+
_common.Catch.call(void 0, ),
|
|
444
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 0, _common.Inject.call(void 0, OAUTH_AUTH_OPTIONS))
|
|
445
|
+
], OAuthCallbackFilter);
|
|
446
|
+
|
|
447
|
+
// src/auth/oauth/oauth.controller.ts
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
function createOAuthController(options = {}) {
|
|
460
|
+
const Guard = _nullishCoalesce(options.guard, () => ( _chunkNVCI3CQIcjs.JwtAuthGuard));
|
|
461
|
+
const userId = _nullishCoalesce(options.userId, () => ( ((user) => user.sub)));
|
|
462
|
+
let OAuthControllerHost = class {
|
|
463
|
+
constructor(auth) {
|
|
464
|
+
this.auth = auth;
|
|
465
|
+
}
|
|
466
|
+
async refresh(request, reply) {
|
|
467
|
+
const raw = _optionalChain([request, 'access', _15 => _15.cookies, 'optionalAccess', _16 => _16[this.auth.config.cookie.name]]);
|
|
468
|
+
if (!raw) throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)("No refresh token.");
|
|
469
|
+
const { accessToken, refreshToken, refreshTokenExpiresAt } = await this.auth.refresh(raw);
|
|
470
|
+
this.writeRefreshCookie(reply, refreshToken, refreshTokenExpiresAt);
|
|
471
|
+
return { accessToken };
|
|
472
|
+
}
|
|
473
|
+
async logout(request, reply) {
|
|
474
|
+
const raw = _optionalChain([request, 'access', _17 => _17.cookies, 'optionalAccess', _18 => _18[this.auth.config.cookie.name]]);
|
|
475
|
+
if (raw) await this.auth.logout(raw);
|
|
476
|
+
clearCookie(reply, this.auth.config.cookie.name, { path: this.auth.config.cookie.path });
|
|
477
|
+
return { success: true };
|
|
478
|
+
}
|
|
479
|
+
me(user) {
|
|
480
|
+
return this.auth.currentUser(userId(user));
|
|
481
|
+
}
|
|
482
|
+
linkedAccounts(user) {
|
|
483
|
+
return this.auth.linkedAccounts(userId(user));
|
|
484
|
+
}
|
|
485
|
+
linkIntent(user, reply) {
|
|
486
|
+
const { token, expiresAt } = this.auth.issueLinkToken(userId(user));
|
|
487
|
+
setCookie(reply, this.auth.config.linkCookieName, token, {
|
|
488
|
+
...this.cookieBase(),
|
|
489
|
+
expires: expiresAt
|
|
490
|
+
});
|
|
491
|
+
return { ok: true };
|
|
492
|
+
}
|
|
493
|
+
start(provider, reply) {
|
|
494
|
+
redirect(reply, this.auth.authorizeUrl(provider));
|
|
495
|
+
}
|
|
496
|
+
async callback(provider, code, request, reply) {
|
|
497
|
+
if (!code) throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)(this.auth.config.genericErrorMessage());
|
|
498
|
+
const profile = await this.auth.profileFromCode(provider, code);
|
|
499
|
+
const linkUserId = this.auth.readLinkToken(_optionalChain([request, 'access', _19 => _19.cookies, 'optionalAccess', _20 => _20[this.auth.config.linkCookieName]]));
|
|
500
|
+
if (linkUserId) {
|
|
501
|
+
clearCookie(reply, this.auth.config.linkCookieName, { path: this.auth.config.cookie.path });
|
|
502
|
+
try {
|
|
503
|
+
await this.auth.linkAccount(linkUserId, provider, profile);
|
|
504
|
+
redirect(reply, this.auth.config.onLinkRedirect(provider));
|
|
505
|
+
} catch (error) {
|
|
506
|
+
const message = error instanceof Error ? error.message : "Could not connect the account.";
|
|
507
|
+
redirect(reply, this.auth.config.onLinkRedirect(provider, message));
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const { accessToken, refreshToken, refreshTokenExpiresAt } = await this.auth.login(
|
|
512
|
+
provider,
|
|
513
|
+
profile
|
|
514
|
+
);
|
|
515
|
+
this.writeRefreshCookie(reply, refreshToken, refreshTokenExpiresAt);
|
|
516
|
+
redirect(reply, this.auth.config.onLoginRedirect(accessToken));
|
|
517
|
+
}
|
|
518
|
+
cookieBase() {
|
|
519
|
+
const { secure, sameSite, path } = this.auth.config.cookie;
|
|
520
|
+
return { httpOnly: true, secure, sameSite, path };
|
|
521
|
+
}
|
|
522
|
+
writeRefreshCookie(reply, token, expiresAt) {
|
|
523
|
+
setCookie(reply, this.auth.config.cookie.name, token, {
|
|
524
|
+
...this.cookieBase(),
|
|
525
|
+
expires: expiresAt
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
_chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
530
|
+
_common.Post.call(void 0, "refresh"),
|
|
531
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 0, _common.Req.call(void 0, )),
|
|
532
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 1, _common.Res.call(void 0, { passthrough: true }))
|
|
533
|
+
], OAuthControllerHost.prototype, "refresh", 1);
|
|
534
|
+
_chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
535
|
+
_common.Post.call(void 0, "logout"),
|
|
536
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 0, _common.Req.call(void 0, )),
|
|
537
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 1, _common.Res.call(void 0, { passthrough: true }))
|
|
538
|
+
], OAuthControllerHost.prototype, "logout", 1);
|
|
539
|
+
_chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
540
|
+
_common.UseGuards.call(void 0, Guard),
|
|
541
|
+
_common.Get.call(void 0, "me"),
|
|
542
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 0, _chunkNVCI3CQIcjs.CurrentUser.call(void 0, ))
|
|
543
|
+
], OAuthControllerHost.prototype, "me", 1);
|
|
544
|
+
_chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
545
|
+
_common.UseGuards.call(void 0, Guard),
|
|
546
|
+
_common.Get.call(void 0, "linked-accounts"),
|
|
547
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 0, _chunkNVCI3CQIcjs.CurrentUser.call(void 0, ))
|
|
548
|
+
], OAuthControllerHost.prototype, "linkedAccounts", 1);
|
|
549
|
+
_chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
550
|
+
_common.UseGuards.call(void 0, Guard),
|
|
551
|
+
_common.Post.call(void 0, "link-intent"),
|
|
552
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 0, _chunkNVCI3CQIcjs.CurrentUser.call(void 0, )),
|
|
553
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 1, _common.Res.call(void 0, { passthrough: true }))
|
|
554
|
+
], OAuthControllerHost.prototype, "linkIntent", 1);
|
|
555
|
+
_chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
556
|
+
_common.Get.call(void 0, ":provider"),
|
|
557
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 0, _common.Param.call(void 0, "provider")),
|
|
558
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 1, _common.Res.call(void 0, ))
|
|
559
|
+
], OAuthControllerHost.prototype, "start", 1);
|
|
560
|
+
_chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
561
|
+
_common.Get.call(void 0, ":provider/callback"),
|
|
562
|
+
_common.UseFilters.call(void 0, OAuthCallbackFilter),
|
|
563
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 0, _common.Param.call(void 0, "provider")),
|
|
564
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 1, _common.Query.call(void 0, "code")),
|
|
565
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 2, _common.Req.call(void 0, )),
|
|
566
|
+
_chunk2REOCMUDcjs.__decorateParam.call(void 0, 3, _common.Res.call(void 0, ))
|
|
567
|
+
], OAuthControllerHost.prototype, "callback", 1);
|
|
568
|
+
OAuthControllerHost = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
569
|
+
_common.Controller.call(void 0, _nullishCoalesce(options.path, () => ( "auth")))
|
|
570
|
+
], OAuthControllerHost);
|
|
571
|
+
return OAuthControllerHost;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/auth/oauth/oauth-auth.module.ts
|
|
575
|
+
|
|
576
|
+
var OAuthAuthModule = class {
|
|
577
|
+
static forRoot(config) {
|
|
578
|
+
return {
|
|
579
|
+
module: OAuthAuthModule,
|
|
580
|
+
imports: _nullishCoalesce(config.imports, () => ( [])),
|
|
581
|
+
providers: [
|
|
582
|
+
{ provide: OAUTH_AUTH_OPTIONS, useValue: config.options },
|
|
583
|
+
...config.providers,
|
|
584
|
+
OAuthAuthService,
|
|
585
|
+
OAuthCallbackFilter
|
|
586
|
+
],
|
|
587
|
+
// The options travel out too: a consumer's own controller needs the
|
|
588
|
+
// cookie name and the redirect builders, and re-deriving them there is
|
|
589
|
+
// how the two copies drift apart.
|
|
590
|
+
exports: [OAuthAuthService, OAuthCallbackFilter, OAUTH_AUTH_OPTIONS]
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
OAuthAuthModule = exports.OAuthAuthModule = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
595
|
+
_common.Module.call(void 0, {})
|
|
596
|
+
], OAuthAuthModule);
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
exports.OAUTH_AUTH_OPTIONS = OAUTH_AUTH_OPTIONS; exports.OAUTH_STORE = OAUTH_STORE; exports.OAuthAuthModule = OAuthAuthModule; exports.OAuthAuthService = OAuthAuthService; exports.OAuthCallbackFilter = OAuthCallbackFilter; exports.REFRESH_TOKEN_STORE = REFRESH_TOKEN_STORE; exports.buildAuthorizeUrl = buildAuthorizeUrl; exports.createOAuthController = createOAuthController; exports.exchangeCodeForToken = exchangeCodeForToken; exports.facebookProvider = facebookProvider; exports.googleProvider = googleProvider; exports.resolveOAuthOptions = resolveOAuthOptions;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { googleProvider, facebookProvider, buildAuthorizeUrl, exchangeCodeForToken, } from './oauth-provider.js';
|
|
2
|
+
export type { OAuthProfile, OAuthProviderConfig, ProviderCredentials, } from './oauth-provider.js';
|
|
3
|
+
export type { OAuthUser, OAuthAccount, OAuthStorePort, RefreshTokenStorePort, ProfileSyncPolicy, } from './oauth.ports.js';
|
|
4
|
+
export { OAUTH_AUTH_OPTIONS, OAUTH_STORE, REFRESH_TOKEN_STORE, resolveOAuthOptions, } from './oauth.options.js';
|
|
5
|
+
export type { OAuthAuthOptions, OAuthCookieOptions, ResolvedOAuthOptions, } from './oauth.options.js';
|
|
6
|
+
export { OAuthAuthService } from './oauth-auth.service.js';
|
|
7
|
+
export type { TokenPair } from './oauth-auth.service.js';
|
|
8
|
+
export { OAuthCallbackFilter } from './oauth-callback.filter.js';
|
|
9
|
+
export { createOAuthController } from './oauth.controller.js';
|
|
10
|
+
export type { CreateOAuthControllerOptions } from './oauth.controller.js';
|
|
11
|
+
export { OAuthAuthModule } from './oauth-auth.module.js';
|
|
12
|
+
export type { OAuthAuthModuleOptions } from './oauth-auth.module.js';
|