@ghuri/socialauth 0.0.0-stage → 0.1.0-alpha.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/dist/index.js ADDED
@@ -0,0 +1,1302 @@
1
+ import {
2
+ SocialAccount,
3
+ SocialApp,
4
+ SocialToken,
5
+ emptyDict
6
+ } from "./chunk-ZNZG4OU3.js";
7
+ import {
8
+ __export
9
+ } from "./chunk-PZ5AY32C.js";
10
+
11
+ // src/adapter.ts
12
+ import { getUserModel } from "@ghuri/auth";
13
+ import { reverse } from "@ghuri/core";
14
+
15
+ // src/settings.ts
16
+ import { tryGetContext } from "@ghuri/core";
17
+ function socialSettings() {
18
+ const settings = tryGetContext()?.settings ?? {};
19
+ const s = settings.SOCIALACCOUNT ?? {};
20
+ return {
21
+ providers: s.PROVIDERS ?? {},
22
+ adapter: s.ADAPTER,
23
+ autoSignup: s.AUTO_SIGNUP ?? true,
24
+ emailAuthentication: s.EMAIL_AUTHENTICATION ?? false,
25
+ emailRequired: s.EMAIL_REQUIRED ?? false,
26
+ loginOnGet: s.LOGIN_ON_GET ?? false,
27
+ storeTokens: s.STORE_TOKENS ?? false,
28
+ loginResponse: s.LOGIN_RESPONSE ?? "session",
29
+ frontendUrl: s.FRONTEND_URL,
30
+ loginErrorUrl: s.LOGIN_ERROR_URL ?? settings.LOGIN_URL ?? "/accounts/login/"
31
+ };
32
+ }
33
+
34
+ // src/adapter.ts
35
+ var ImmediateResponse = class extends Error {
36
+ constructor(response) {
37
+ super("ImmediateResponse");
38
+ this.response = response;
39
+ }
40
+ response;
41
+ };
42
+ var USERNAME_ALLOWED = /[^\w.@+-]/g;
43
+ var SocialAccountAdapter = class {
44
+ /** May new users sign up through social login? */
45
+ isOpenForSignup(_c, _login) {
46
+ return true;
47
+ }
48
+ /** Called after the provider answered, before anything is looked up or created. */
49
+ async preSocialLogin(_c, _login) {
50
+ }
51
+ /** May the user be created without showing the signup form? */
52
+ isAutoSignupAllowed(_c, login) {
53
+ const s = socialSettings();
54
+ if (!s.autoSignup) return false;
55
+ if (s.emailRequired && !login.profile.email) return false;
56
+ return true;
57
+ }
58
+ /** A new, unsaved user instance. */
59
+ newUser(_c, _login) {
60
+ const User = getUserModel();
61
+ return new User();
62
+ }
63
+ /** Copy profile data (and signup form data) onto the new user. */
64
+ populateUser(_c, login, user, data) {
65
+ const M = user.constructor;
66
+ const has = (f) => !!M._meta.getField?.(f);
67
+ if (data.username !== void 0) user[M.USERNAME_FIELD] = data.username;
68
+ if (data.email !== void 0 && has(M.EMAIL_FIELD)) user[M.EMAIL_FIELD] = data.email;
69
+ if (has("firstName") && login.profile.firstName) user.firstName = login.profile.firstName.slice(0, 150);
70
+ if (has("lastName") && login.profile.lastName) user.lastName = login.profile.lastName.slice(0, 150);
71
+ if (has("firstName") && !login.profile.firstName && login.profile.name && has("lastName")) {
72
+ const [first, ...rest] = login.profile.name.trim().split(/\s+/);
73
+ user.firstName = (first ?? "").slice(0, 150);
74
+ user.lastName = rest.join(" ").slice(0, 150);
75
+ }
76
+ return user;
77
+ }
78
+ /** Save the new user (with an unusable password). */
79
+ async saveUser(_c, _login, user) {
80
+ if (typeof user.setUnusablePassword === "function") user.setUnusablePassword();
81
+ await user.save();
82
+ return user;
83
+ }
84
+ /** Username candidates, best first. */
85
+ usernameCandidates(login) {
86
+ const p = login.profile;
87
+ return [p.username, p.email?.split("@")[0], p.name, p.firstName, `${login.key}${p.id}`].filter(
88
+ (v) => typeof v === "string" && v.trim().length > 0
89
+ );
90
+ }
91
+ /** A username that is free, from the profile. */
92
+ async generateUsername(_c, login) {
93
+ const User = getUserModel();
94
+ const field = User.USERNAME_FIELD;
95
+ const maxLength = User._meta.getField(field)?.maxLength ?? 150;
96
+ const taken = async (name) => User.objects.filter({ [`${field}__iexact`]: name }).exists();
97
+ for (const raw of this.usernameCandidates(login)) {
98
+ const base = raw.trim().replace(/\s+/g, "").replace(USERNAME_ALLOWED, "").slice(0, maxLength);
99
+ if (!base) continue;
100
+ if (!await taken(base)) return base;
101
+ for (let i = 2; i < 100; i++) {
102
+ const suffix = String(i);
103
+ const candidate = `${base.slice(0, maxLength - suffix.length)}${suffix}`;
104
+ if (!await taken(candidate)) return candidate;
105
+ }
106
+ }
107
+ return `user${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
108
+ }
109
+ /** Where to go after connecting an account. */
110
+ getConnectRedirectUrl(_c, _account) {
111
+ try {
112
+ return reverse("socialaccount_connections");
113
+ } catch {
114
+ return "/";
115
+ }
116
+ }
117
+ /** Customize error responses; return undefined for the default. */
118
+ authenticationError(_c, _error) {
119
+ return void 0;
120
+ }
121
+ };
122
+ var cache = /* @__PURE__ */ new WeakMap();
123
+ var fallback;
124
+ function getAdapter() {
125
+ const cls = socialSettings().adapter;
126
+ if (!cls) {
127
+ fallback ??= new SocialAccountAdapter();
128
+ return fallback;
129
+ }
130
+ let a = cache.get(cls);
131
+ if (!a) {
132
+ a = new cls();
133
+ cache.set(cls, a);
134
+ }
135
+ return a;
136
+ }
137
+
138
+ // src/apps.ts
139
+ import * as auth from "@ghuri/auth";
140
+ import { AppConfig, apps, ImproperlyConfigured } from "@ghuri/core";
141
+
142
+ // src/migrations/index.ts
143
+ var migrations_exports = {};
144
+ __export(migrations_exports, {
145
+ m0001_initial: () => initial_default,
146
+ migrations: () => migrations
147
+ });
148
+
149
+ // src/migrations/0001_initial.ts
150
+ import { Migration, operations as ops } from "@ghuri/orm/migrations";
151
+ var initial_default = new Migration({
152
+ initial: true,
153
+ dependencies: [["auth", "0001_initial"]],
154
+ operations: [
155
+ ops.CreateModel({
156
+ name: "SocialAccount",
157
+ fields: {
158
+ id: { type: "BigAutoField", primaryKey: true },
159
+ user: {
160
+ type: "ForeignKey",
161
+ to: "auth.User",
162
+ onDelete: "CASCADE",
163
+ dbIndex: true,
164
+ relatedName: "socialAccounts",
165
+ verboseName: "user"
166
+ },
167
+ provider: { type: "CharField", maxLength: 200, verboseName: "provider" },
168
+ uid: { type: "CharField", maxLength: 191, verboseName: "uid" },
169
+ lastLogin: { type: "DateTimeField", autoNow: true, verboseName: "last login" },
170
+ dateJoined: { type: "DateTimeField", autoNowAdd: true, verboseName: "date joined" },
171
+ extraData: {
172
+ type: "JSONField",
173
+ default: { callable: "emptyDict" },
174
+ verboseName: "extra data"
175
+ }
176
+ },
177
+ options: {
178
+ dbTable: "socialaccount_socialaccount",
179
+ verboseName: "social account",
180
+ verboseNamePlural: "social accounts",
181
+ uniqueTogether: [["provider", "uid"]]
182
+ }
183
+ }),
184
+ ops.CreateModel({
185
+ name: "SocialApp",
186
+ fields: {
187
+ id: { type: "BigAutoField", primaryKey: true },
188
+ provider: { type: "CharField", maxLength: 30, verboseName: "provider" },
189
+ providerId: { type: "CharField", maxLength: 200, blank: true, verboseName: "provider ID" },
190
+ name: { type: "CharField", maxLength: 40, verboseName: "name" },
191
+ clientId: { type: "CharField", maxLength: 191, verboseName: "client id" },
192
+ secret: { type: "CharField", maxLength: 191, blank: true, verboseName: "secret key" },
193
+ key: { type: "CharField", maxLength: 191, blank: true, verboseName: "key" },
194
+ settings: {
195
+ type: "JSONField",
196
+ blank: true,
197
+ default: { callable: "emptyDict" },
198
+ verboseName: "settings"
199
+ }
200
+ },
201
+ options: {
202
+ dbTable: "socialaccount_socialapp",
203
+ verboseName: "social application",
204
+ verboseNamePlural: "social applications"
205
+ }
206
+ }),
207
+ ops.CreateModel({
208
+ name: "SocialToken",
209
+ fields: {
210
+ id: { type: "BigAutoField", primaryKey: true },
211
+ app: {
212
+ type: "ForeignKey",
213
+ to: "socialaccount.SocialApp",
214
+ onDelete: "CASCADE",
215
+ null: true,
216
+ blank: true,
217
+ dbIndex: true
218
+ },
219
+ account: {
220
+ type: "ForeignKey",
221
+ to: "socialaccount.SocialAccount",
222
+ onDelete: "CASCADE",
223
+ dbIndex: true,
224
+ relatedName: "tokens"
225
+ },
226
+ token: { type: "TextField", verboseName: "token" },
227
+ tokenSecret: { type: "TextField", blank: true, verboseName: "token secret" },
228
+ expiresAt: { type: "DateTimeField", null: true, blank: true, verboseName: "expires at" }
229
+ },
230
+ options: {
231
+ dbTable: "socialaccount_socialtoken",
232
+ verboseName: "social application token",
233
+ verboseNamePlural: "social application tokens",
234
+ uniqueTogether: [["app", "account"]]
235
+ }
236
+ })
237
+ ]
238
+ });
239
+
240
+ // src/migrations/index.ts
241
+ var migrations = {
242
+ "0001_initial": initial_default
243
+ };
244
+
245
+ // src/apps.ts
246
+ function appPath() {
247
+ const meta = import.meta;
248
+ const dir = meta.dirname;
249
+ if (!dir || !/[\\/]src$/.test(dir) || !/\.[cm]?ts$/.test(meta.filename ?? "")) return void 0;
250
+ return dir;
251
+ }
252
+ function migrationsModule() {
253
+ const adapt = auth.migrationsForUserModel;
254
+ return typeof adapt === "function" ? adapt("socialaccount", migrations_exports) : migrations_exports;
255
+ }
256
+ var SocialAuthConfig = class extends AppConfig {
257
+ name = "@ghuri/socialauth";
258
+ label = "socialaccount";
259
+ verboseName = "Social Accounts";
260
+ path = appPath();
261
+ migrations = migrationsModule();
262
+ models = { SocialApp, SocialAccount, SocialToken };
263
+ ready() {
264
+ if (!apps.isInstalled("auth")) {
265
+ throw new ImproperlyConfigured(
266
+ "'socialaccount' requires '@ghuri/auth': add AuthConfig (and ContentTypesConfig) to INSTALLED_APPS."
267
+ );
268
+ }
269
+ }
270
+ };
271
+
272
+ // src/login.ts
273
+ import { login as authLogin, getUserModel as getUserModel2, isSafeRedirect, tokenPairForUser } from "@ghuri/auth";
274
+ import { cache as cache2, getHost, getUser, isSecure, resolveUrl, reverse as reverse2, tryGetContext as tryGetContext2 } from "@ghuri/core";
275
+ import {
276
+ loginBackendName,
277
+ randomToken,
278
+ seal as seal2,
279
+ unseal as unseal2,
280
+ userInfo
281
+ } from "@ghuri/oauth";
282
+ import { deleteCookie, getCookie, setCookie } from "hono/cookie";
283
+
284
+ // src/signals.ts
285
+ import { Signal } from "@ghuri/core";
286
+ var g = globalThis;
287
+ function singleton(key, make) {
288
+ const k = Symbol.for(key);
289
+ if (!g[k]) g[k] = make();
290
+ return g[k];
291
+ }
292
+ var preSocialLogin = singleton(
293
+ "ghuri.socialauth.preSocialLogin",
294
+ () => new Signal("preSocialLogin")
295
+ );
296
+ var socialAccountAdded = singleton("ghuri.socialauth.socialAccountAdded", () => new Signal("socialAccountAdded"));
297
+ var socialAccountUpdated = singleton("ghuri.socialauth.socialAccountUpdated", () => new Signal("socialAccountUpdated"));
298
+ var socialAccountRemoved = singleton(
299
+ "ghuri.socialauth.socialAccountRemoved",
300
+ () => new Signal("socialAccountRemoved")
301
+ );
302
+
303
+ // src/tokens.ts
304
+ import { OAuth2Strategy as OAuth2Strategy2, seal, unseal } from "@ghuri/oauth";
305
+
306
+ // src/providers.ts
307
+ import { ImproperlyConfigured as ImproperlyConfigured2 } from "@ghuri/core";
308
+ import {
309
+ getJson,
310
+ OAuth2Strategy,
311
+ OAuthError,
312
+ OIDCStrategy,
313
+ profileFromClaims,
314
+ providerFetch,
315
+ readBody
316
+ } from "@ghuri/oauth";
317
+ var Provider = class {
318
+ /** Several apps per provider, each with its own `providerId` (OpenID Connect). */
319
+ multipleApps = false;
320
+ /** Default scopes when `SCOPE` is not set. */
321
+ defaultScope = [];
322
+ scope(settings) {
323
+ return settings.SCOPE ?? this.defaultScope;
324
+ }
325
+ /** May this provider's email be trusted to belong to the user? */
326
+ emailVerified(profile, settings) {
327
+ if (settings.VERIFIED_EMAIL !== void 0) return settings.VERIFIED_EMAIL && !!profile.email;
328
+ return profile.emailVerified === true && !!profile.email;
329
+ }
330
+ /**
331
+ * Native / mobile login: verify a token the app obtained with the provider's own SDK and return the profile.
332
+ * The token must have been issued to one of our client ids. Default: ID tokens of OpenID Connect providers.
333
+ */
334
+ async verifyNativeToken(ctx, tokens) {
335
+ const strategy = this.createStrategy(ctx);
336
+ if (!(strategy instanceof OIDCStrategy) || !tokens.idToken) {
337
+ throw new OAuthError(`${this.name} login with a native token needs an id_token.`, "unsupported");
338
+ }
339
+ const audiences = [ctx.app.clientId, ...ctx.app.settings?.audiences ?? []];
340
+ let claims;
341
+ let lastError;
342
+ for (const aud of audiences) {
343
+ try {
344
+ const s = this.createStrategy({ ...ctx, app: { ...ctx.app, clientId: aud } });
345
+ claims = await s.validateIdToken(tokens.idToken);
346
+ break;
347
+ } catch (err) {
348
+ lastError = err;
349
+ }
350
+ }
351
+ if (!claims)
352
+ throw lastError instanceof Error ? new OAuthError(lastError.message, "invalid_token") : new OAuthError("Invalid ID token.");
353
+ return this.profileFromClaims(ctx.key, claims);
354
+ }
355
+ profileFromClaims(key, claims) {
356
+ return profileFromClaims(key, claims);
357
+ }
358
+ };
359
+ function common(ctx, provider2) {
360
+ return {
361
+ name: ctx.key,
362
+ clientId: ctx.app.clientId,
363
+ clientSecret: ctx.app.secret,
364
+ callbackURL: ctx.callbackURL,
365
+ scope: provider2.scope(ctx.settings),
366
+ authorizationParams: ctx.settings.AUTH_PARAMS
367
+ };
368
+ }
369
+ function oidc(ctx, provider2, issuer, extra = {}) {
370
+ return new OIDCStrategy(
371
+ {
372
+ ...common(ctx, provider2),
373
+ issuer,
374
+ profile: async (tokens) => provider2.profileFromClaims(ctx.key, tokens.raw.claims),
375
+ ...extra
376
+ },
377
+ ctx.verify
378
+ );
379
+ }
380
+ var GitHubProvider = class extends Provider {
381
+ id = "github";
382
+ name = "GitHub";
383
+ defaultScope = ["read:user", "user:email"];
384
+ api(ctx) {
385
+ return String(ctx.app.settings?.apiUrl ?? "https://api.github.com").replace(/\/+$/, "");
386
+ }
387
+ createStrategy(ctx) {
388
+ const web = String(ctx.app.settings?.webUrl ?? "https://github.com").replace(/\/+$/, "");
389
+ const api = this.api(ctx);
390
+ return new OAuth2Strategy(
391
+ {
392
+ ...common(ctx, this),
393
+ authorizationURL: `${web}/login/oauth/authorize`,
394
+ tokenURL: `${web}/login/oauth/access_token`,
395
+ profile: async (_tokens, p) => this.profile(ctx.key, api, (url) => p.getJsonAuthed(url))
396
+ },
397
+ ctx.verify
398
+ );
399
+ }
400
+ async profile(key, api, get) {
401
+ const user = await get(`${api}/user`);
402
+ let email = user.email ?? void 0;
403
+ let verified = false;
404
+ try {
405
+ const emails = await get(`${api}/user/emails`);
406
+ const list = Array.isArray(emails) ? emails : [];
407
+ const primary = list.find((e) => e.primary && e.verified) ?? list.find((e) => e.verified);
408
+ if (primary) {
409
+ email = primary.email;
410
+ verified = true;
411
+ }
412
+ } catch {
413
+ }
414
+ return {
415
+ provider: key,
416
+ id: String(user.id),
417
+ username: user.login,
418
+ name: user.name ?? void 0,
419
+ email,
420
+ emailVerified: verified,
421
+ avatarUrl: user.avatar_url,
422
+ raw: user
423
+ };
424
+ }
425
+ /** Checks with GitHub that the token belongs to our OAuth app (`POST /applications/{client_id}/token`). */
426
+ async verifyNativeToken(ctx, tokens) {
427
+ if (!tokens.accessToken)
428
+ throw new OAuthError("GitHub login with a native token needs an access_token.", "unsupported");
429
+ const api = this.api(ctx);
430
+ const res = await providerFetch(`${api}/applications/${encodeURIComponent(ctx.app.clientId)}/token`, {
431
+ method: "POST",
432
+ headers: {
433
+ authorization: `Basic ${btoa(`${ctx.app.clientId}:${ctx.app.secret ?? ""}`)}`,
434
+ accept: "application/vnd.github+json",
435
+ "content-type": "application/json"
436
+ },
437
+ body: JSON.stringify({ access_token: tokens.accessToken })
438
+ });
439
+ if (!res.ok) throw new OAuthError("GitHub did not accept the access token for this app.", "invalid_token");
440
+ const headers = { authorization: `Bearer ${tokens.accessToken}` };
441
+ return this.profile(ctx.key, api, (url) => getJson(url, { headers }));
442
+ }
443
+ };
444
+ var GoogleProvider = class extends Provider {
445
+ id = "google";
446
+ name = "Google";
447
+ defaultScope = ["openid", "email", "profile"];
448
+ createStrategy(ctx) {
449
+ return oidc(ctx, this, "https://accounts.google.com");
450
+ }
451
+ };
452
+ var FacebookProvider = class extends Provider {
453
+ id = "facebook";
454
+ name = "Facebook";
455
+ defaultScope = ["email", "public_profile"];
456
+ graph(ctx) {
457
+ return `https://graph.facebook.com/${String(ctx.settings.VERSION ?? ctx.app.settings?.version ?? "v21.0")}`;
458
+ }
459
+ createStrategy(ctx) {
460
+ const version = String(ctx.settings.VERSION ?? ctx.app.settings?.version ?? "v21.0");
461
+ const graph = this.graph(ctx);
462
+ return new OAuth2Strategy(
463
+ {
464
+ ...common(ctx, this),
465
+ scopeSeparator: ",",
466
+ authorizationURL: `https://www.facebook.com/${version}/dialog/oauth`,
467
+ tokenURL: `${graph}/oauth/access_token`,
468
+ profile: async (_t, p) => this.profile(
469
+ ctx.key,
470
+ await p.getJsonAuthed(`${graph}/me?fields=id,name,email,first_name,last_name,picture`)
471
+ )
472
+ },
473
+ ctx.verify
474
+ );
475
+ }
476
+ profile(key, me) {
477
+ return {
478
+ provider: key,
479
+ id: String(me.id),
480
+ name: me.name,
481
+ firstName: me.first_name,
482
+ lastName: me.last_name,
483
+ email: me.email,
484
+ // Facebook does not say whether the address was confirmed: unverified unless VERIFIED_EMAIL.
485
+ emailVerified: false,
486
+ avatarUrl: me.picture?.data?.url,
487
+ raw: me
488
+ };
489
+ }
490
+ /** Checks the token with `debug_token` (issued to our app id) before trusting it. */
491
+ async verifyNativeToken(ctx, tokens) {
492
+ if (!tokens.accessToken)
493
+ throw new OAuthError("Facebook login with a native token needs an access_token.", "unsupported");
494
+ const graph = this.graph(ctx);
495
+ const appToken = `${ctx.app.clientId}|${ctx.app.secret ?? ""}`;
496
+ const debug = await getJson(
497
+ `${graph}/debug_token?input_token=${encodeURIComponent(tokens.accessToken)}&access_token=${encodeURIComponent(appToken)}`
498
+ );
499
+ if (!debug.data?.is_valid || String(debug.data.app_id) !== ctx.app.clientId) {
500
+ throw new OAuthError("The Facebook token was not issued to this app.", "invalid_token");
501
+ }
502
+ const me = await getJson(`${graph}/me?fields=id,name,email,first_name,last_name,picture`, {
503
+ headers: { authorization: `Bearer ${tokens.accessToken}` }
504
+ });
505
+ return this.profile(ctx.key, me);
506
+ }
507
+ };
508
+ var MicrosoftProvider = class extends Provider {
509
+ id = "microsoft";
510
+ name = "Microsoft";
511
+ defaultScope = ["openid", "email", "profile", "User.Read"];
512
+ createStrategy(ctx) {
513
+ const tenant = String(ctx.app.settings?.tenant ?? ctx.settings.TENANT ?? "common");
514
+ const multi = ["common", "organizations", "consumers"].includes(tenant);
515
+ return oidc(ctx, this, `https://login.microsoftonline.com/${tenant}/v2.0`, {
516
+ // Multi-tenant endpoints publish `{tenantid}` in their issuer; accept any tenant's issuer.
517
+ issuerValidation: multi ? (iss) => /^https:\/\/login\.microsoftonline\.com\/[0-9a-f-]{36}\/v2\.0$/.test(iss) : void 0,
518
+ fetchUserInfo: false
519
+ });
520
+ }
521
+ profileFromClaims(key, claims) {
522
+ const p = profileFromClaims(key, claims);
523
+ return { ...p, id: String(claims.oid ?? claims.sub), emailVerified: false };
524
+ }
525
+ emailVerified(profile, settings) {
526
+ return settings.VERIFIED_EMAIL === true && !!profile.email;
527
+ }
528
+ };
529
+ var DiscordProvider = class extends Provider {
530
+ id = "discord";
531
+ name = "Discord";
532
+ defaultScope = ["identify", "email"];
533
+ createStrategy(ctx) {
534
+ return new OAuth2Strategy(
535
+ {
536
+ ...common(ctx, this),
537
+ authorizationURL: "https://discord.com/oauth2/authorize",
538
+ tokenURL: "https://discord.com/api/oauth2/token",
539
+ profile: async (_t, p) => {
540
+ const me = await p.getJsonAuthed("https://discord.com/api/users/@me");
541
+ return {
542
+ provider: ctx.key,
543
+ id: String(me.id),
544
+ username: me.username,
545
+ name: me.global_name ?? me.username,
546
+ email: me.email ?? void 0,
547
+ emailVerified: me.verified === true,
548
+ avatarUrl: me.avatar ? `https://cdn.discordapp.com/avatars/${me.id}/${me.avatar}.png` : void 0,
549
+ raw: me
550
+ };
551
+ }
552
+ },
553
+ ctx.verify
554
+ );
555
+ }
556
+ async verifyNativeToken() {
557
+ throw new OAuthError(
558
+ "Discord tokens cannot be checked against a client id; use the redirect flow.",
559
+ "unsupported"
560
+ );
561
+ }
562
+ };
563
+ var GitLabProvider = class extends Provider {
564
+ id = "gitlab";
565
+ name = "GitLab";
566
+ defaultScope = ["openid", "email", "profile"];
567
+ createStrategy(ctx) {
568
+ const url = String(ctx.app.settings?.url ?? ctx.settings.GITLAB_URL ?? "https://gitlab.com").replace(
569
+ /\/+$/,
570
+ ""
571
+ );
572
+ return oidc(ctx, this, url);
573
+ }
574
+ profileFromClaims(key, claims) {
575
+ return { ...profileFromClaims(key, claims), username: claims.nickname ?? claims.preferred_username };
576
+ }
577
+ };
578
+ var OpenIDConnectProvider = class extends Provider {
579
+ id = "openid_connect";
580
+ name = "OpenID Connect";
581
+ multipleApps = true;
582
+ defaultScope = ["openid", "email", "profile"];
583
+ createStrategy(ctx) {
584
+ const issuer = ctx.app.settings?.serverUrl ?? ctx.app.settings?.server_url;
585
+ if (typeof issuer !== "string") {
586
+ throw new ImproperlyConfigured2(
587
+ `The OpenID Connect app '${ctx.key}' needs settings.serverUrl (the issuer).`
588
+ );
589
+ }
590
+ return oidc(ctx, this, issuer);
591
+ }
592
+ };
593
+ var KEY = /* @__PURE__ */ Symbol.for("ghuri.socialauth.providers");
594
+ var registry = (() => {
595
+ const g2 = globalThis;
596
+ if (!g2[KEY]) {
597
+ const m = /* @__PURE__ */ new Map();
598
+ for (const p of [
599
+ new GitHubProvider(),
600
+ new GoogleProvider(),
601
+ new FacebookProvider(),
602
+ new MicrosoftProvider(),
603
+ new DiscordProvider(),
604
+ new GitLabProvider(),
605
+ new OpenIDConnectProvider()
606
+ ]) {
607
+ m.set(p.id, p);
608
+ }
609
+ g2[KEY] = m;
610
+ }
611
+ return g2[KEY];
612
+ })();
613
+ function registerProvider(provider2) {
614
+ registry.set(provider2.id, provider2);
615
+ }
616
+ function getProvider(id) {
617
+ return registry.get(id);
618
+ }
619
+ function allProviders() {
620
+ return [...registry.values()];
621
+ }
622
+
623
+ // src/resolve.ts
624
+ function fromRow(row) {
625
+ return {
626
+ clientId: row.clientId,
627
+ secret: row.secret || void 0,
628
+ key: row.key || void 0,
629
+ providerId: row.providerId || void 0,
630
+ name: row.name,
631
+ settings: row.settings ?? {}
632
+ };
633
+ }
634
+ async function safeRows(filter) {
635
+ try {
636
+ return await SocialApp.objects.filter(filter).orderBy("id");
637
+ } catch {
638
+ return [];
639
+ }
640
+ }
641
+ async function resolveProvider(key) {
642
+ const all = socialSettings().providers;
643
+ const direct = getProvider(key);
644
+ if (direct && !direct.multipleApps) {
645
+ const settings = all[key] ?? {};
646
+ if (settings.APP) return { key, provider: direct, app: settings.APP, settings };
647
+ const [row] = await safeRows({ provider: key });
648
+ return row ? { key, provider: direct, app: fromRow(row), settings, row } : null;
649
+ }
650
+ for (const provider2 of allProviders().filter((p) => p.multipleApps)) {
651
+ const settings = all[provider2.id] ?? {};
652
+ const app = settings.APPS?.find((a) => a.providerId === key);
653
+ if (app) return { key, provider: provider2, app, settings };
654
+ const [row] = await safeRows({ provider: provider2.id, providerId: key });
655
+ if (row) return { key, provider: provider2, app: fromRow(row), settings, row };
656
+ }
657
+ return null;
658
+ }
659
+ async function configuredProviders() {
660
+ const all = socialSettings().providers;
661
+ const out = [];
662
+ const seen = /* @__PURE__ */ new Set();
663
+ for (const provider2 of allProviders()) {
664
+ const settings = all[provider2.id] ?? {};
665
+ if (provider2.multipleApps) {
666
+ for (const app of settings.APPS ?? []) {
667
+ if (app.providerId && !seen.has(app.providerId)) {
668
+ seen.add(app.providerId);
669
+ out.push({ key: app.providerId, provider: provider2, app, settings });
670
+ }
671
+ }
672
+ for (const row of await safeRows({ provider: provider2.id })) {
673
+ if (row.providerId && !seen.has(row.providerId)) {
674
+ seen.add(row.providerId);
675
+ out.push({ key: row.providerId, provider: provider2, app: fromRow(row), settings, row });
676
+ }
677
+ }
678
+ } else if (settings.APP) {
679
+ seen.add(provider2.id);
680
+ out.push({ key: provider2.id, provider: provider2, app: settings.APP, settings });
681
+ }
682
+ }
683
+ for (const row of await safeRows({})) {
684
+ const provider2 = getProvider(row.provider);
685
+ if (provider2 && !provider2.multipleApps && !seen.has(row.provider)) {
686
+ seen.add(row.provider);
687
+ out.push({ key: row.provider, provider: provider2, app: fromRow(row), settings: all[row.provider] ?? {}, row });
688
+ }
689
+ }
690
+ return out;
691
+ }
692
+
693
+ // src/tokens.ts
694
+ var PURPOSE = "socialauth-token";
695
+ async function storeTokens(login, account) {
696
+ if (!socialSettings().storeTokens || !login.tokens?.accessToken) return;
697
+ const t = login.tokens;
698
+ const app = login.row ?? null;
699
+ const existing = await SocialToken.objects.filter({ account, app }).first();
700
+ const values = {
701
+ token: await seal(t.accessToken, PURPOSE),
702
+ tokenSecret: t.refreshToken ? await seal(t.refreshToken, PURPOSE) : existing?.tokenSecret ?? "",
703
+ expiresAt: t.expiresAt ? new Date(t.expiresAt) : null
704
+ };
705
+ if (existing) {
706
+ Object.assign(existing, values);
707
+ await existing.save();
708
+ } else {
709
+ await SocialToken.objects.create({ account, app, ...values });
710
+ }
711
+ }
712
+ async function getAccessToken(account) {
713
+ const row = await SocialToken.objects.filter({ account }).first();
714
+ if (!row) return null;
715
+ const access = await unseal(row.token, PURPOSE);
716
+ const expiresAt = row.expiresAt ? new Date(row.expiresAt).getTime() : void 0;
717
+ if (access && (!expiresAt || expiresAt - 3e4 > Date.now())) return access;
718
+ const refresh = row.tokenSecret ? await unseal(row.tokenSecret, PURPOSE) : null;
719
+ if (!refresh) return null;
720
+ const resolved = await resolveProvider(account.provider);
721
+ if (!resolved) return null;
722
+ const strategy = resolved.provider.createStrategy({
723
+ key: resolved.key,
724
+ app: resolved.app,
725
+ settings: resolved.settings,
726
+ callbackURL: "/",
727
+ verify: async () => null
728
+ });
729
+ if (!(strategy instanceof OAuth2Strategy2)) return null;
730
+ let tokens;
731
+ try {
732
+ tokens = await (await strategy.client()).refresh(refresh);
733
+ } catch {
734
+ return null;
735
+ }
736
+ row.token = await seal(tokens.accessToken, PURPOSE);
737
+ if (tokens.refreshToken) row.tokenSecret = await seal(tokens.refreshToken, PURPOSE);
738
+ row.expiresAt = tokens.expiresAt ? new Date(tokens.expiresAt) : null;
739
+ await row.save();
740
+ return tokens.accessToken;
741
+ }
742
+
743
+ // src/login.ts
744
+ var SocialAuthError = class extends Error {
745
+ constructor(message, status = 400, code = "social_login_failed") {
746
+ super(message);
747
+ this.status = status;
748
+ this.code = code;
749
+ }
750
+ status;
751
+ code;
752
+ };
753
+ function buildLogin(resolved, profile, opts) {
754
+ return {
755
+ ...resolved,
756
+ profile: { ...profile, provider: resolved.key },
757
+ tokens: opts.tokens,
758
+ emailVerified: resolved.provider.emailVerified(profile, resolved.settings),
759
+ process: opts.process ?? "login",
760
+ next: opts.next
761
+ };
762
+ }
763
+ function loginRedirectUrl() {
764
+ return resolveUrl(
765
+ tryGetContext2()?.settings.LOGIN_REDIRECT_URL ?? "/accounts/profile/"
766
+ );
767
+ }
768
+ var CODE_PURPOSE = "socialauth-code";
769
+ var CODE_TTL = 60;
770
+ async function makeLoginCode(user) {
771
+ return seal2(
772
+ JSON.stringify({
773
+ uid: String(user.pk),
774
+ exp: Math.floor(Date.now() / 1e3) + CODE_TTL,
775
+ jti: randomToken(12)
776
+ }),
777
+ CODE_PURPOSE
778
+ );
779
+ }
780
+ async function redeemLoginCode(code) {
781
+ const raw = await unseal2(code, CODE_PURPOSE);
782
+ if (!raw) return null;
783
+ let data;
784
+ try {
785
+ data = JSON.parse(raw);
786
+ } catch {
787
+ return null;
788
+ }
789
+ if (!data.exp || data.exp < Date.now() / 1e3) return null;
790
+ const key = `ghuri:socialauth:code:${data.jti}`;
791
+ try {
792
+ if (await cache2.get(key)) return null;
793
+ await cache2.set(key, 1, CODE_TTL * 2);
794
+ } catch {
795
+ }
796
+ const User = getUserModel2();
797
+ const user = await User.objects.filter({ pk: User._meta.pk.toPython(data.uid) }).first();
798
+ return user && user.isActive !== false ? user : null;
799
+ }
800
+ async function finishLogin(c, user, login, mode) {
801
+ const s = socialSettings();
802
+ if (user.isActive === false) throw new SocialAuthError("This account is inactive.", 403, "inactive");
803
+ if (s.loginResponse === "jwt") {
804
+ if (mode === "api") return c.json({ ...await tokenPairForUser(user), user: userInfo(user) });
805
+ if (!s.frontendUrl)
806
+ throw new SocialAuthError('SOCIALACCOUNT.FRONTEND_URL is required with LOGIN_RESPONSE "jwt".', 500);
807
+ return c.redirect(`${s.frontendUrl}#code=${encodeURIComponent(await makeLoginCode(user))}`, 302);
808
+ }
809
+ await authLogin(c, user, loginBackendName());
810
+ if (mode === "api") return c.json({ user: userInfo(user) });
811
+ const next = login.next;
812
+ return c.redirect(isSafeRedirect(next, [getHost(c)]) ? next : loginRedirectUrl(), 302);
813
+ }
814
+ async function errorResponse(c, err, provider2, mode) {
815
+ const custom = await getAdapter().authenticationError(c, { provider: provider2, message: err.message, code: err.code });
816
+ if (custom) return custom;
817
+ if (mode === "api") return c.json({ detail: err.message, code: err.code }, err.status);
818
+ const s = socialSettings();
819
+ if (s.loginResponse === "jwt" && s.frontendUrl) {
820
+ return c.redirect(`${s.frontendUrl}#error=${encodeURIComponent(err.message)}`, 302);
821
+ }
822
+ const url = new URL(s.loginErrorUrl, "http://x");
823
+ url.searchParams.set("error", err.message);
824
+ return c.redirect(`${url.pathname}${url.search}`, 302);
825
+ }
826
+ var SIGNUP_COOKIE = "ghuri_social_signup";
827
+ var SIGNUP_PURPOSE = "socialauth-signup";
828
+ var SIGNUP_TTL = 600;
829
+ async function savePendingSignup(c, login) {
830
+ const pending = {
831
+ key: login.key,
832
+ profile: login.profile,
833
+ tokens: socialSettings().storeTokens ? login.tokens : void 0,
834
+ emailVerified: login.emailVerified,
835
+ next: login.next,
836
+ createdAt: Date.now()
837
+ };
838
+ setCookie(c, SIGNUP_COOKIE, await seal2(JSON.stringify(pending), SIGNUP_PURPOSE), {
839
+ path: "/",
840
+ httpOnly: true,
841
+ secure: isSecure(c),
842
+ sameSite: "Lax",
843
+ maxAge: SIGNUP_TTL
844
+ });
845
+ }
846
+ async function readPendingSignup(c) {
847
+ const raw = getCookie(c, SIGNUP_COOKIE);
848
+ if (!raw) return null;
849
+ const json = await unseal2(raw, SIGNUP_PURPOSE);
850
+ if (!json) return null;
851
+ try {
852
+ const p = JSON.parse(json);
853
+ return Date.now() - p.createdAt <= SIGNUP_TTL * 1e3 ? p : null;
854
+ } catch {
855
+ return null;
856
+ }
857
+ }
858
+ function expirePendingSignup(c, res) {
859
+ res.headers.append(
860
+ "set-cookie",
861
+ `${SIGNUP_COOKIE}=; Max-Age=0; Path=/; HttpOnly; SameSite=Lax${isSecure(c) ? "; Secure" : ""}`
862
+ );
863
+ return res;
864
+ }
865
+ async function emailTaken(email, exceptPk) {
866
+ const User = getUserModel2();
867
+ let qs = User.objects.filter({ [`${User.EMAIL_FIELD}__iexact`]: email });
868
+ if (exceptPk !== void 0) qs = qs.exclude({ pk: exceptPk });
869
+ return qs.exists();
870
+ }
871
+ async function link(c, login, user) {
872
+ const account = await SocialAccount.objects.create({
873
+ user,
874
+ provider: login.key,
875
+ uid: login.profile.id,
876
+ extraData: login.profile.raw
877
+ });
878
+ await storeTokens(login, account);
879
+ await socialAccountAdded.send(SocialAccount, { c, login, account });
880
+ return account;
881
+ }
882
+ async function signup(c, login, data, mode) {
883
+ const adapter = getAdapter();
884
+ const User = getUserModel2();
885
+ const errors = {};
886
+ const username = (data.username ?? "").trim();
887
+ const email = (data.email ?? "").trim();
888
+ if (User.USERNAME_FIELD !== User.EMAIL_FIELD) {
889
+ if (!username) errors[User.USERNAME_FIELD] = ["This field is required."];
890
+ else if (await User.objects.filter({ [`${User.USERNAME_FIELD}__iexact`]: username }).exists()) {
891
+ errors[User.USERNAME_FIELD] = ["A user with that username already exists."];
892
+ }
893
+ }
894
+ if (!email && (socialSettings().emailRequired || User.USERNAME_FIELD === User.EMAIL_FIELD)) {
895
+ errors[User.EMAIL_FIELD] = ["This field is required."];
896
+ } else if (email && await emailTaken(email)) {
897
+ errors[User.EMAIL_FIELD] = [
898
+ `An account already uses this address. Log in to it first, then connect ${login.provider.name} from your account.`
899
+ ];
900
+ }
901
+ if (Object.keys(errors).length) return errors;
902
+ let user = adapter.newUser(c, login);
903
+ user = adapter.populateUser(c, login, user, {
904
+ username: User.USERNAME_FIELD === User.EMAIL_FIELD ? email : username,
905
+ email: email || void 0
906
+ });
907
+ user = await adapter.saveUser(c, login, user);
908
+ await link(c, login, user);
909
+ return finishLogin(c, user, login, mode);
910
+ }
911
+ async function completeSocialLogin(c, login, mode) {
912
+ const adapter = getAdapter();
913
+ await preSocialLogin.send(SocialAccount, { c, login });
914
+ await adapter.preSocialLogin(c, login);
915
+ const current = getUser(c);
916
+ const User = getUserModel2();
917
+ const existing = await SocialAccount.objects.filter({ provider: login.key, uid: login.profile.id }).first();
918
+ if (existing) {
919
+ existing.extraData = login.profile.raw;
920
+ await existing.save();
921
+ await storeTokens(login, existing);
922
+ await socialAccountUpdated.send(SocialAccount, { c, login, account: existing });
923
+ const userId = existing.userId ?? existing.user_id;
924
+ if (login.process === "connect") {
925
+ if (!current.isAuthenticated)
926
+ throw new SocialAuthError("Log in before connecting an account.", 401, "login_required");
927
+ if (String(userId) !== String(current.pk)) {
928
+ throw new SocialAuthError(
929
+ `This ${login.provider.name} account is already connected to another user.`,
930
+ 409,
931
+ "already_connected"
932
+ );
933
+ }
934
+ return mode === "api" ? c.json({ connected: true }) : c.redirect(adapter.getConnectRedirectUrl(c, existing), 302);
935
+ }
936
+ const user = await User.objects.filter({ pk: userId }).first();
937
+ if (!user) throw new SocialAuthError("The linked user no longer exists.", 401, "user_missing");
938
+ return finishLogin(c, user, login, mode);
939
+ }
940
+ if (login.process === "connect") {
941
+ if (!current.isAuthenticated)
942
+ throw new SocialAuthError("Log in before connecting an account.", 401, "login_required");
943
+ const user = await User.objects.filter({ pk: current.pk }).first();
944
+ const account = await link(c, login, user);
945
+ return mode === "api" ? c.json({ connected: true }) : c.redirect(adapter.getConnectRedirectUrl(c, account), 302);
946
+ }
947
+ const s = socialSettings();
948
+ if (s.emailAuthentication && login.emailVerified && login.profile.email) {
949
+ const matches = await User.objects.filter({ [`${User.EMAIL_FIELD}__iexact`]: login.profile.email }).limit(2);
950
+ if (matches.length === 1) {
951
+ await link(c, login, matches[0]);
952
+ return finishLogin(c, matches[0], login, mode);
953
+ }
954
+ }
955
+ if (!await adapter.isOpenForSignup(c, login)) {
956
+ throw new SocialAuthError("Sign-ups are closed.", 403, "signup_closed");
957
+ }
958
+ if (await adapter.isAutoSignupAllowed(c, login)) {
959
+ const email = login.profile.email && !await emailTaken(login.profile.email) ? login.profile.email : void 0;
960
+ if (email || !s.emailRequired && !login.profile.email && User.USERNAME_FIELD !== User.EMAIL_FIELD) {
961
+ const result = await signup(
962
+ c,
963
+ login,
964
+ {
965
+ username: User.USERNAME_FIELD === User.EMAIL_FIELD ? email : await adapter.generateUsername(c, login),
966
+ email
967
+ },
968
+ mode
969
+ );
970
+ if (result instanceof Response) return result;
971
+ }
972
+ }
973
+ await savePendingSignup(c, login);
974
+ let signupUrl = "/accounts/signup/";
975
+ try {
976
+ signupUrl = reverse2("socialaccount_signup");
977
+ } catch {
978
+ }
979
+ if (mode === "api") {
980
+ return c.json({ detail: "Finish signing up.", code: "signup_required", signupUrl }, 409);
981
+ }
982
+ return c.redirect(signupUrl, 302);
983
+ }
984
+
985
+ // src/views.ts
986
+ import { getUserModel as getUserModel3, readData, tokenPairForUser as tokenPairForUser2 } from "@ghuri/auth";
987
+ import {
988
+ csrfExempt,
989
+ escapeHtml,
990
+ getCsrfToken,
991
+ getUser as getUser2,
992
+ path,
993
+ reverse as reverse3,
994
+ urlpatterns,
995
+ wantsJson
996
+ } from "@ghuri/core";
997
+ import { userInfo as userInfo2 } from "@ghuri/oauth";
998
+ function page(title, body, status = 200) {
999
+ const html = `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title><style>body{font:16px/1.5 system-ui,sans-serif;max-width:28rem;margin:4rem auto;padding:0 1rem}button{font:inherit;padding:.5rem 1rem;cursor:pointer}input{font:inherit;width:100%;padding:.4rem;margin:.2rem 0 .8rem;box-sizing:border-box}.err{color:#b91c1c}</style></head><body><h1>${escapeHtml(title)}</h1>${body}</body></html>`;
1000
+ return new Response(html, { status, headers: { "content-type": "text/html; charset=utf-8" } });
1001
+ }
1002
+ function csrfInput(c) {
1003
+ try {
1004
+ return `<input type="hidden" name="csrfmiddlewaretoken" value="${escapeHtml(getCsrfToken(c))}">`;
1005
+ } catch {
1006
+ return "";
1007
+ }
1008
+ }
1009
+ function str(v) {
1010
+ return typeof v === "string" && v ? v : void 0;
1011
+ }
1012
+ async function provider(c) {
1013
+ const key = c.req.param("provider") ?? "";
1014
+ const resolved = await resolveProvider(key);
1015
+ if (!resolved)
1016
+ throw new SocialAuthError(`Unknown or unconfigured provider '${key}'.`, 404, "unknown_provider");
1017
+ return resolved;
1018
+ }
1019
+ function strategyFor(c, resolved) {
1020
+ const callbackURL = c.req.path.replace(/login\/(callback\/)?$/, "login/callback/");
1021
+ const override = globalThis[/* @__PURE__ */ Symbol.for("ghuri.socialauth.testStrategy")];
1022
+ const fake = override?.(resolved.key, callbackURL);
1023
+ if (fake) return fake;
1024
+ return resolved.provider.createStrategy({
1025
+ key: resolved.key,
1026
+ app: resolved.app,
1027
+ settings: resolved.settings,
1028
+ callbackURL,
1029
+ // The social flow decides what happens next; the strategy only has to succeed.
1030
+ verify: async () => ({ socialLogin: true })
1031
+ });
1032
+ }
1033
+ function guarded(mode, fn) {
1034
+ return async (c) => {
1035
+ const m = typeof mode === "function" ? mode(c) : mode;
1036
+ try {
1037
+ return await fn(c);
1038
+ } catch (err) {
1039
+ if (err instanceof ImmediateResponse) return err.response;
1040
+ if (err instanceof SocialAuthError) return errorResponse(c, err, c.req.param("provider") ?? "", m);
1041
+ throw err;
1042
+ }
1043
+ };
1044
+ }
1045
+ function requireJson(c) {
1046
+ if (!(c.req.header("content-type") ?? "").includes("application/json")) {
1047
+ throw new SocialAuthError("Send a JSON body (Content-Type: application/json).", 415, "json_required");
1048
+ }
1049
+ }
1050
+ var providersView = async (c) => {
1051
+ const list = await configuredProviders();
1052
+ return c.json({
1053
+ providers: list.map((p) => ({
1054
+ id: p.key,
1055
+ name: p.app.name ?? p.provider.name,
1056
+ provider: p.provider.id,
1057
+ loginUrl: reverse3("socialaccount_login", { provider: p.key }),
1058
+ nativeTokenUrl: reverse3("socialaccount_native_token", { provider: p.key })
1059
+ }))
1060
+ });
1061
+ };
1062
+ var loginView = guarded("browser", async (c) => {
1063
+ const resolved = await provider(c);
1064
+ const s = socialSettings();
1065
+ if (c.req.method === "GET" && !s.loginOnGet) {
1066
+ if (wantsJson(c)) return c.json({ detail: "POST to this URL to log in.", method: "POST" }, 405);
1067
+ const action = escapeHtml(c.req.url.replace(/^[a-z]+:\/\/[^/]+/i, ""));
1068
+ const name = escapeHtml(resolved.app.name ?? resolved.provider.name);
1069
+ return page(
1070
+ `Continue with ${resolved.app.name ?? resolved.provider.name}`,
1071
+ `<p>You are about to log in with ${name}.</p><form method="post" action="${action}">${csrfInput(c)}<button type="submit">Continue</button></form>`
1072
+ );
1073
+ }
1074
+ if (c.req.method !== "GET" && c.req.method !== "POST") return c.text("Method not allowed", 405);
1075
+ const data = c.req.method === "POST" ? await readData(c) : {};
1076
+ const process = (str(data.process) ?? c.req.query("process")) === "connect" ? "connect" : "login";
1077
+ if (process === "connect" && !getUser2(c).isAuthenticated) {
1078
+ throw new SocialAuthError("Log in before connecting an account.", 401, "login_required");
1079
+ }
1080
+ const result = await strategyFor(c, resolved).authenticate(c, {
1081
+ next: str(data.next) ?? c.req.query("next"),
1082
+ state: { process }
1083
+ });
1084
+ if (result.type === "redirect") return c.redirect(result.url, 302);
1085
+ if (result.type === "error") throw result.error;
1086
+ throw new SocialAuthError(
1087
+ result.type === "fail" ? result.message ?? "Login failed." : "Login failed.",
1088
+ 400
1089
+ );
1090
+ });
1091
+ var callbackView = guarded("browser", async (c) => {
1092
+ const resolved = await provider(c);
1093
+ if (!c.req.query("code") && !c.req.query("error")) {
1094
+ throw new SocialAuthError("The provider did not send an authorization code.", 400, "missing_code");
1095
+ }
1096
+ const result = await strategyFor(c, resolved).authenticate(c, {});
1097
+ if (result.type === "error") throw result.error;
1098
+ if (result.type !== "success") {
1099
+ throw new SocialAuthError(
1100
+ result.type === "fail" ? result.message ?? "Login failed." : "Login failed.",
1101
+ 401
1102
+ );
1103
+ }
1104
+ const info = result.info;
1105
+ const login = buildLogin(resolved, info.profile, {
1106
+ tokens: info.tokens,
1107
+ next: info.next,
1108
+ process: info.state?.process === "connect" ? "connect" : "login"
1109
+ });
1110
+ return completeSocialLogin(c, login, "browser");
1111
+ });
1112
+ var nativeTokenView = csrfExempt(
1113
+ guarded("api", async (c) => {
1114
+ if (c.req.method !== "POST") return c.json({ detail: "Method not allowed" }, 405);
1115
+ requireJson(c);
1116
+ const resolved = await provider(c);
1117
+ const body = await c.req.json().catch(() => ({}));
1118
+ const idToken = str(body.id_token);
1119
+ const accessToken = str(body.access_token);
1120
+ if (!idToken && !accessToken)
1121
+ throw new SocialAuthError("Send id_token or access_token.", 400, "missing_token");
1122
+ let profile;
1123
+ try {
1124
+ profile = await resolved.provider.verifyNativeToken(
1125
+ {
1126
+ key: resolved.key,
1127
+ app: resolved.app,
1128
+ settings: resolved.settings,
1129
+ callbackURL: "/",
1130
+ verify: async () => null
1131
+ },
1132
+ { idToken, accessToken }
1133
+ );
1134
+ } catch (err) {
1135
+ throw new SocialAuthError(err.message || "Invalid token.", 401, "invalid_token");
1136
+ }
1137
+ const login = buildLogin(resolved, profile, {
1138
+ tokens: accessToken ? { accessToken, raw: {} } : void 0,
1139
+ process: str(body.process) === "connect" ? "connect" : "login"
1140
+ });
1141
+ return completeSocialLogin(c, login, "api");
1142
+ })
1143
+ );
1144
+ var tokenExchangeView = csrfExempt(
1145
+ guarded("api", async (c) => {
1146
+ if (c.req.method !== "POST") return c.json({ detail: "Method not allowed" }, 405);
1147
+ requireJson(c);
1148
+ const body = await c.req.json().catch(() => ({}));
1149
+ const user = str(body.code) ? await redeemLoginCode(str(body.code)) : null;
1150
+ if (!user)
1151
+ throw new SocialAuthError("This login code is invalid, expired or already used.", 400, "invalid_code");
1152
+ return c.json({ ...await tokenPairForUser2(user), user: userInfo2(user) });
1153
+ })
1154
+ );
1155
+ var signupView = guarded(
1156
+ (c) => wantsJson(c) ? "api" : "browser",
1157
+ async (c) => {
1158
+ const pending = await readPendingSignup(c);
1159
+ if (!pending)
1160
+ throw new SocialAuthError("There is no social signup in progress.", 400, "no_pending_signup");
1161
+ const resolved = await resolveProvider(pending.key);
1162
+ if (!resolved) throw new SocialAuthError("The provider is no longer configured.", 400, "unknown_provider");
1163
+ const login = buildLogin(resolved, pending.profile, { tokens: pending.tokens, next: pending.next });
1164
+ const User = getUserModel3();
1165
+ const uf = User.USERNAME_FIELD;
1166
+ const ef = User.EMAIL_FIELD;
1167
+ const json = wantsJson(c);
1168
+ const render = (values2, errors = {}, status = 200) => {
1169
+ if (json) {
1170
+ return c.json(
1171
+ {
1172
+ provider: pending.key,
1173
+ profile: {
1174
+ email: pending.profile.email,
1175
+ username: pending.profile.username,
1176
+ name: pending.profile.name
1177
+ },
1178
+ fields: [.../* @__PURE__ */ new Set([uf, ef])],
1179
+ errors
1180
+ },
1181
+ status
1182
+ );
1183
+ }
1184
+ const field = (name, label) => `<label>${escapeHtml(label)}<input name="${escapeHtml(name)}" value="${escapeHtml(values2[name] ?? "")}"></label>${(errors[name] ?? []).map((e) => `<p class="err">${escapeHtml(e)}</p>`).join("")}`;
1185
+ const fields = uf === ef ? field(ef, "Email") : field(uf, "Username") + field(ef, "Email");
1186
+ return page(
1187
+ "Finish signing up",
1188
+ `<p>You are signing up with ${escapeHtml(resolved.app.name ?? resolved.provider.name)}.</p><form method="post">${csrfInput(c)}${fields}<button type="submit">Sign up</button></form>`,
1189
+ status
1190
+ );
1191
+ };
1192
+ if (c.req.method === "GET") {
1193
+ return render({ [uf]: pending.profile.username ?? "", [ef]: pending.profile.email ?? "" });
1194
+ }
1195
+ const data = await readData(c);
1196
+ const values = { [uf]: String(data[uf] ?? ""), [ef]: String(data[ef] ?? "") };
1197
+ if (values[ef] && values[ef].toLowerCase() !== (pending.profile.email ?? "").toLowerCase())
1198
+ login.emailVerified = false;
1199
+ const result = await signup(
1200
+ c,
1201
+ login,
1202
+ { username: values[uf], email: values[ef] },
1203
+ json ? "api" : "browser"
1204
+ );
1205
+ if (result instanceof Response) return expirePendingSignup(c, result);
1206
+ return render(values, result, 400);
1207
+ }
1208
+ );
1209
+ var connectionsView = guarded(
1210
+ (c) => wantsJson(c) ? "api" : "browser",
1211
+ async (c) => {
1212
+ const user = getUser2(c);
1213
+ if (!user.isAuthenticated)
1214
+ throw new SocialAuthError("Log in to manage your connected accounts.", 401, "login_required");
1215
+ const accounts = await SocialAccount.objects.filter({ user: user.pk }).orderBy("provider", "id");
1216
+ if (c.req.method === "POST") {
1217
+ const data = await readData(c);
1218
+ const id = String(data.account ?? "");
1219
+ const account = accounts.find((a) => String(a.pk) === id);
1220
+ if (!account) throw new SocialAuthError("No such connected account.", 404, "not_found");
1221
+ const User = getUserModel3();
1222
+ const full = await User.objects.filter({ pk: user.pk }).first();
1223
+ if (accounts.length === 1 && full && typeof full.hasUsablePassword === "function" && !full.hasUsablePassword()) {
1224
+ throw new SocialAuthError(
1225
+ "Set a password before disconnecting your last connected account.",
1226
+ 400,
1227
+ "last_login_method"
1228
+ );
1229
+ }
1230
+ await account.delete();
1231
+ await socialAccountRemoved.send(SocialAccount, { c, account });
1232
+ if (wantsJson(c)) return c.json({ disconnected: id });
1233
+ return c.redirect(c.req.path, 302);
1234
+ }
1235
+ const list = accounts.map((a) => ({
1236
+ id: a.pk,
1237
+ provider: a.provider,
1238
+ uid: a.uid,
1239
+ lastLogin: a.lastLogin,
1240
+ dateJoined: a.dateJoined
1241
+ }));
1242
+ if (wantsJson(c)) return c.json({ accounts: list });
1243
+ const rows = list.map(
1244
+ (a) => `<li>${escapeHtml(a.provider)} <form method="post" style="display:inline">${csrfInput(c)}<input type="hidden" name="account" value="${escapeHtml(a.id)}"><button type="submit">Disconnect</button></form></li>`
1245
+ ).join("");
1246
+ return page("Connected accounts", rows ? `<ul>${rows}</ul>` : "<p>No connected accounts.</p>");
1247
+ }
1248
+ );
1249
+ var socialUrls = urlpatterns(void 0, [
1250
+ path("providers/", providersView, { name: "socialaccount_providers" }),
1251
+ path("token/", tokenExchangeView, { name: "socialaccount_token_exchange" }),
1252
+ path("signup/", signupView, { name: "socialaccount_signup" }),
1253
+ path("connections/", connectionsView, { name: "socialaccount_connections" }),
1254
+ path("<str:provider>/login/", loginView, { name: "socialaccount_login" }),
1255
+ path("<str:provider>/login/callback/", callbackView, { name: "socialaccount_callback" }),
1256
+ path("<str:provider>/token/", nativeTokenView, { name: "socialaccount_native_token" })
1257
+ ]);
1258
+ export {
1259
+ DiscordProvider,
1260
+ FacebookProvider,
1261
+ GitHubProvider,
1262
+ GitLabProvider,
1263
+ GoogleProvider,
1264
+ ImmediateResponse,
1265
+ MicrosoftProvider,
1266
+ OpenIDConnectProvider,
1267
+ Provider,
1268
+ SocialAccount,
1269
+ SocialAccountAdapter,
1270
+ SocialApp,
1271
+ SocialAuthConfig,
1272
+ SocialAuthError,
1273
+ SocialToken,
1274
+ allProviders,
1275
+ buildLogin,
1276
+ callbackView,
1277
+ completeSocialLogin,
1278
+ configuredProviders,
1279
+ connectionsView,
1280
+ emptyDict,
1281
+ finishLogin,
1282
+ getAccessToken,
1283
+ getAdapter,
1284
+ getProvider,
1285
+ loginView,
1286
+ makeLoginCode,
1287
+ nativeTokenView,
1288
+ preSocialLogin,
1289
+ providersView,
1290
+ redeemLoginCode,
1291
+ registerProvider,
1292
+ resolveProvider,
1293
+ signupView,
1294
+ socialAccountAdded,
1295
+ socialAccountRemoved,
1296
+ socialAccountUpdated,
1297
+ socialSettings,
1298
+ socialUrls,
1299
+ storeTokens,
1300
+ tokenExchangeView
1301
+ };
1302
+ //# sourceMappingURL=index.js.map