Skip to content

Commit

Permalink
feat: upsert User when reading session info
Browse files Browse the repository at this point in the history
  • Loading branch information
exKAZUu committed Jan 8, 2024
1 parent b8923bb commit fe408f5
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 142 deletions.
Binary file removed prisma/dev.sqlite3
Binary file not shown.
Binary file removed prisma/dev.sqlite3-journal
Binary file not shown.
5 changes: 5 additions & 0 deletions src/app/(withAuth)/courses/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { NextPage } from 'next';

import { getSessionOnServerPage } from '../../../infrastructures/session';

const CoursePage: NextPage = async () => {
const session = await getSessionOnServerPage();

return (
<main>
<div>UserID on Supertokens: {session.getUserId()}</div>
<h1>Courses</h1>
</main>
);
Expand Down
4 changes: 2 additions & 2 deletions src/app/(withAuth)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { SessionAuthForNext } from '../../components/molecules/SessionAuthForNex
import { TryRefreshComponent } from '../../components/molecules/TryRefreshComponent';
import { DefaultFooter } from '../../components/organisms/DefaultFooter';
import { DefaultHeader } from '../../components/organisms/DefaultHeader';
import { getSessionOnServerLayout } from '../../infrastructures/session';
import type { LayoutProps } from '../../types';
import { getSSRSession } from '../sessionUtils';

const DefaultLayout: NextPage<LayoutProps> = async ({ children }) => {
const { hasInvalidClaims, hasToken, session } = await getSSRSession();
const { hasInvalidClaims, hasToken, session } = await getSessionOnServerLayout();

// `session` will be undefined if it does not exist or has expired
if (!session) {
Expand Down
139 changes: 0 additions & 139 deletions src/app/sessionUtils.ts

This file was deleted.

117 changes: 117 additions & 0 deletions src/infrastructures/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { cookies, headers } from 'next/headers';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { CollectingResponse, PreParsedRequest } from 'supertokens-node/framework/custom';
import type { SessionContainer, VerifySessionOptions } from 'supertokens-node/recipe/session';
import Session from 'supertokens-node/recipe/session';
import type { HTTPMethod } from 'supertokens-node/types';

import { logger } from './pino';
import { prisma } from './prisma';
import { ensureSuperTokensInit } from './supertokens/backendConfig';

ensureSuperTokensInit();

export interface SessionOnServerLayout {
session: SessionContainer | undefined;
hasToken: boolean;
/**
* This allows us to protect our routes based on the current session claims.
* For example this will be true if email verification is required but the user has not verified their email.
*/
hasInvalidClaims: boolean;
baseResponse: CollectingResponse;
nextResponse?: NextResponse;
}

export async function getSessionOnServerLayout(
req?: NextRequest,
options?: VerifySessionOptions
): Promise<SessionOnServerLayout> {
const baseRequest = getBaseRequest(req);

// Collecting response is a wrapper exposed by SuperTokens. In this case we are using an empty
// CollectingResponse when calling `getSession`. If the request contains valid session tokens
// the SuperTokens SDK will attach all the relevant tokens to the collecting response object which
// we can then use to return those session tokens in the final result (refer to `withSession` in this file)
const baseResponse = new CollectingResponse();

try {
// `getSession` will throw if session is required and there is no valid session. You can use
// `options` to configure whether or not you want to require sessions when calling `getSSRSession`
const session = await Session.getSession(baseRequest, baseResponse, options);

// Update User table with Prisma
if (session) {
await upsertUserToPrisma(session.getUserId());
}

return {
session,
hasInvalidClaims: false,
hasToken: session !== undefined,
baseResponse,
};
} catch (error) {
if (Session.Error.isErrorFromSuperTokens(error)) {
return {
hasToken: error.type !== Session.Error.UNAUTHORISED,
hasInvalidClaims: error.type === Session.Error.INVALID_CLAIMS,
session: undefined,
baseResponse,
nextResponse: new NextResponse('Authentication required', {
status: error.type === Session.Error.INVALID_CLAIMS ? 403 : 401,
}),
};
} else {
throw error;
}
}
}

export async function getSessionOnServerPage(
req?: NextRequest,
options?: VerifySessionOptions
): Promise<SessionContainer> {
const baseRequest = getBaseRequest(req);
const baseResponse = new CollectingResponse();
const session = await Session.getSession(baseRequest, baseResponse, options);
if (!session) {
throw new Error('Failed to get a session.');
}
return session;
}

function getBaseRequest(req: NextRequest | undefined): PreParsedRequest {
const query = req === undefined ? {} : Object.fromEntries(new URL(req.url).searchParams.entries());
const parsedCookies: Record<string, string> = Object.fromEntries(
(req === undefined ? cookies() : req.cookies).getAll().map((cookie) => [cookie.name, cookie.value])
);

// Pre parsed request is a wrapper exposed by SuperTokens. It is used as a helper to detect if the
// original request contains session tokens. We then use this pre parsed request to call `getSession`
// to check if there is a valid session.
return new PreParsedRequest({
method: req === undefined ? 'get' : (req.method as HTTPMethod),
url: req === undefined ? '' : req.url,
query,
headers: req === undefined ? headers() : req.headers,
cookies: parsedCookies,
getFormBody: () => req!.formData(),
getJSONBody: () => req!.json(),
});
}

const upsertedUserIds = new Set<string>();

async function upsertUserToPrisma(superTokensUserId: string): Promise<void> {
if (upsertedUserIds.has(superTokensUserId)) return;

upsertedUserIds.add(superTokensUserId);
const user = await prisma.user.upsert({
create: { superTokensUserId },
update: {},
where: { superTokensUserId },
});
logger.debug('User upserted: %o', user);
}
5 changes: 4 additions & 1 deletion src/infrastructures/supertokens/backendConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ export const backendConfig = (): TypeInput => {
};

let initialized = false;
// This function is used in your APIs to make sure SuperTokens is initialised

/**
* This function is used in your APIs to make sure SuperTokens is initialised
*/
export function ensureSuperTokensInit(): void {
if (!initialized) {
SuperTokens.init(backendConfig());
Expand Down

0 comments on commit fe408f5

Please sign in to comment.