Skip to content

Commit 3362061

Browse files
committed
fix(server): pass valid sign in to context server side
1 parent 67d56fb commit 3362061

File tree

8 files changed

+64
-30
lines changed

8 files changed

+64
-30
lines changed

apps/nextjs-kitchensink/app/invites/InvitesIndex.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default async function InvitesIndex() {
99
const me = await nile.users.getSelf();
1010

1111
return (
12+
cssonsole.log(me, 'wtf?');
1213
<EnsureSignedIn me={me}>
1314
<TenantsAndTables me={me as unknown as User} />
1415
</EnsureSignedIn>

apps/nextjs-kitchensink/app/verify-email/page.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export default async function VerifyEmailPage() {
5757
);
5858
}
5959

60+
// not common, but update the users without tenant context, because that's what I want to do.
6061
async function unVerifyEmail() {
6162
'use server';
6263
const me = await nile.users.getSelf();
@@ -66,10 +67,12 @@ async function unVerifyEmail() {
6667
error: 'Not logged in',
6768
};
6869
}
69-
await nile.db.query(
70-
'update users.users set email_verified = NULL where id = $1',
71-
[me.id]
72-
);
70+
await nile.withContext({ ddl: true }, async (ddl) => {
71+
await ddl.db.query(
72+
'update users.users set email_verified = NULL where id = $1',
73+
[me.id]
74+
);
75+
});
7376
revalidatePath('/verify-email');
7477
return { ok: true };
7578
}

packages/server/src/Server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ export class Server {
5959

6060
watchHeaders((headers) => {
6161
if (headers) {
62+
// internally we can call this for sign in, among other things. Be sure the next request still works.
6263
this.#config.context.headers = new Headers(headers);
63-
// this.setContext(headers);
64+
// this is always true when called internally
65+
this.#config.context.preserveHeaders = true;
6466
this.#reset();
6567
}
6668
});

packages/server/src/api/utils/request-context.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,17 @@ export const ctx: CTX = {
5757
if (partial.headers === null) {
5858
store.headers = new Headers();
5959
} else if (partial.headers && store.headers instanceof Headers) {
60-
for (const [k, v] of new Headers(partial.headers).entries()) {
61-
store.headers.set(k, v);
60+
for (const [key, value] of new Headers(partial.headers).entries()) {
61+
if (key.toLowerCase() === 'cookie') {
62+
const existingCookies = parseCookieHeader(
63+
store.headers.get('cookie') || ''
64+
);
65+
const newCookies = parseCookieHeader(value);
66+
const mergedCookies = { ...existingCookies, ...newCookies };
67+
store.headers.set('cookie', serializeCookies(mergedCookies));
68+
} else {
69+
store.headers.set(key, value);
70+
}
6271
}
6372
}
6473

@@ -148,3 +157,21 @@ function serializeContext(context: Context): string {
148157
preserveHeaders: context.preserveHeaders,
149158
});
150159
}
160+
161+
function parseCookieHeader(header: string): Record<string, string> {
162+
return header
163+
.split(';')
164+
.map((c) => c.trim())
165+
.filter(Boolean)
166+
.reduce((acc, curr) => {
167+
const [key, ...val] = curr.split('=');
168+
if (key) acc[key] = val.join('=');
169+
return acc;
170+
}, {} as Record<string, string>);
171+
}
172+
173+
function serializeCookies(cookies: Record<string, string>): string {
174+
return Object.entries(cookies)
175+
.map(([k, v]) => `${k}=${v}`)
176+
.join('; ');
177+
}

packages/server/src/auth/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,7 @@ export default class Auth {
565565
.join('; ');
566566
const uHeaders = new Headers({ cookie });
567567
updateHeaders(uHeaders);
568-
ctx.set({ headers: uHeaders });
568+
ctx.set({ headers: uHeaders, preserveHeaders: true });
569569
} else {
570570
error('Unable to set context after sign in', {
571571
headers: signInRes.headers,

packages/server/src/tenants/index.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import obtainCsrf from '../auth/obtainCsrf';
1616
import { NileRequest } from '../types';
1717
import { User } from '../users/types';
1818
import { Config } from '../utils/Config';
19+
import { fQUrl } from '../utils/qualifyDomain';
1920

2021
import { Invite, Tenant } from './types';
2122

@@ -360,11 +361,12 @@ export default class Tenants {
360361
identifier = req.email;
361362
}
362363

364+
const { callbackUrl: cbUrl } = defaultCallbackUrl(this.#config);
363365
if ('callbackUrl' in req) {
364-
callbackUrl = fQUrl(req.callbackUrl ?? '', this.#config);
366+
callbackUrl = fQUrl(cbUrl, req.callbackUrl ?? '/');
365367
}
366368
if ('redirectUrl' in req) {
367-
redirectUrl = fQUrl(req.redirectUrl ?? '', this.#config);
369+
redirectUrl = fQUrl(cbUrl, req.redirectUrl ?? '/');
368370
}
369371
}
370372

@@ -492,19 +494,3 @@ export function defaultCallbackUrl(config: Config) {
492494
}
493495
return { callbackUrl: cb, redirectUrl: redirect };
494496
}
495-
496-
function fQUrl(path: string, config: Config) {
497-
if (path.startsWith('/')) {
498-
const { callbackUrl } = defaultCallbackUrl(config);
499-
if (callbackUrl) {
500-
const { origin } = new URL(callbackUrl);
501-
return `${origin}${path}`;
502-
}
503-
}
504-
try {
505-
new URL(path);
506-
} catch {
507-
throw new Error('An invalid URL has been passed.');
508-
}
509-
return path;
510-
}

packages/server/src/users/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import getCsrf from '../auth/obtainCsrf';
66
import { Loggable } from '../utils/Logger';
77
import { parseCallback } from '../auth';
88
import { ctx, withNileContext } from '../api/utils/request-context';
9+
import { fQUrl } from '../utils/qualifyDomain';
910

1011
import { User } from './types';
1112

@@ -132,10 +133,10 @@ export default class Users {
132133
return withNileContext(this.#config, async () => {
133134
const bypassEmail =
134135
typeof options === 'object' && options?.bypassEmail === true;
135-
const callbackUrl =
136-
typeof options === 'object'
137-
? options.callbackUrl
138-
: defaultCallbackUrl().callbackUrl;
136+
const callbackUrl = fQUrl(
137+
defaultCallbackUrl().callbackUrl,
138+
typeof options === 'object' ? String(options.callbackUrl) : '/'
139+
);
139140

140141
let res;
141142

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function fQUrl(callbackUrl: null | string, path: string) {
2+
if (path.startsWith('/')) {
3+
if (callbackUrl) {
4+
const { origin } = new URL(callbackUrl);
5+
return `${origin}${path}`;
6+
}
7+
}
8+
try {
9+
new URL(path);
10+
} catch {
11+
throw new Error('An invalid URL has been passed.');
12+
}
13+
return path;
14+
}

0 commit comments

Comments
 (0)