Add back vc auth temporarily to unblock things
This commit is contained in:
parent
07c2e7d36a
commit
177b2cc210
20 changed files with 350 additions and 40 deletions
25
lib/cookies/decrypt-jwe.ts
Normal file
25
lib/cookies/decrypt-jwe.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { jwtDecrypt, base64url } from 'jose';
|
||||
|
||||
export async function decryptJWECookie<T extends string | object = any>(
|
||||
cookie: string,
|
||||
secret: string | undefined = process.env.JWE_SECRET
|
||||
): Promise<T | undefined> {
|
||||
if (!secret) {
|
||||
throw new Error('Missing JWE secret');
|
||||
}
|
||||
|
||||
if (typeof cookie !== 'string') return;
|
||||
|
||||
try {
|
||||
const { payload } = await jwtDecrypt(cookie, base64url.decode(secret));
|
||||
const decoded = payload as T;
|
||||
|
||||
// @ts-expect-error jwt field
|
||||
delete decoded.iat;
|
||||
// @ts-expect-error jwt field
|
||||
delete decoded.exp;
|
||||
return decoded;
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
16
lib/cookies/encrypt-jwe.ts
Normal file
16
lib/cookies/encrypt-jwe.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { EncryptJWT, base64url } from 'jose';
|
||||
|
||||
export async function encryptJWECookie<T extends string | object = any>(
|
||||
payload: T,
|
||||
expirationTime: string,
|
||||
secret: string | undefined = process.env.JWE_SECRET
|
||||
): Promise<string> {
|
||||
if (!secret) {
|
||||
throw new Error('Missing JWE secret');
|
||||
}
|
||||
|
||||
return new EncryptJWT(payload as any)
|
||||
.setExpirationTime(expirationTime)
|
||||
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
|
||||
.encrypt(base64url.decode(secret));
|
||||
}
|
||||
1
lib/session/constants.ts
Normal file
1
lib/session/constants.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const userSessionCookieName = 'user_session';
|
||||
116
lib/session/create.ts
Normal file
116
lib/session/create.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { type Session } from './types';
|
||||
|
||||
export async function createSession(vercelToken: string): Promise<Session> {
|
||||
const [user, teams] = await Promise.all([
|
||||
fetchUser(vercelToken),
|
||||
fetchTeams(vercelToken),
|
||||
]);
|
||||
const plan = getHighestAccountLevel(teams);
|
||||
return {
|
||||
user: {
|
||||
id: user.uid,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
avatar: `https://vercel.com/api/www/avatar/?u=${user.username}`,
|
||||
plan,
|
||||
},
|
||||
vercelToken,
|
||||
};
|
||||
}
|
||||
|
||||
interface VercelUser {
|
||||
uid: string;
|
||||
username: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
type BillingStatus = 'active' | 'trialing' | 'overdue' | 'canceled' | 'expired';
|
||||
|
||||
export const billingPlans = ['hobby', 'pro', 'enterprise'] as const;
|
||||
|
||||
export type BillingPlan = (typeof billingPlans)[number];
|
||||
|
||||
export interface Billing {
|
||||
addons?: string | null;
|
||||
language?: string | null;
|
||||
plan: BillingPlan;
|
||||
// TODO: Make this required once the migration has completed
|
||||
status?: BillingStatus;
|
||||
name?: string | null;
|
||||
overdue?: boolean | null;
|
||||
period: { start: number; end: number } | null;
|
||||
purchaseOrder?: string | null;
|
||||
platform?: 'stripe' | 'stripeTestMode';
|
||||
email?: string;
|
||||
trial: { start: number; end: number } | null;
|
||||
}
|
||||
export interface VercelTeam {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
billing?: Billing;
|
||||
created?: string;
|
||||
}
|
||||
async function fetchUser(token: string) {
|
||||
const response = await fetch('https://vercel.com/api/user', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const { user } = (await response.json()) as { user: VercelUser };
|
||||
|
||||
return user;
|
||||
}
|
||||
export async function fetchTeams(token: string) {
|
||||
const response = await fetch('https://vercel.com/api/teams', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const { teams } = (await response.json()) as { teams: VercelTeam[] };
|
||||
return teams;
|
||||
}
|
||||
|
||||
export function hasActiveProOrEnterpriseAccount(teams: VercelTeam[]) {
|
||||
return teams.some(
|
||||
(team) =>
|
||||
(team.billing?.plan === 'pro' && team.billing.status === 'active') ||
|
||||
(team.billing?.plan === 'enterprise' && team.billing.status === 'active')
|
||||
);
|
||||
}
|
||||
|
||||
export function hasActiveProAccount(teams: VercelTeam[]) {
|
||||
return teams.some(
|
||||
(team) => team.billing?.plan === 'pro' && team.billing.status === 'active'
|
||||
);
|
||||
}
|
||||
|
||||
export function isInTrial(teams: VercelTeam[]) {
|
||||
return (
|
||||
!hasActiveProOrEnterpriseAccount(teams) &&
|
||||
teams.some(
|
||||
(team) =>
|
||||
team.billing?.plan === 'pro' && team.billing.status === 'trialing'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function getHighestAccountLevel(teams: VercelTeam[]): BillingPlan {
|
||||
const plans = teams
|
||||
.filter(
|
||||
(team) => team.billing?.plan && team.billing.status === 'active'
|
||||
// (team.billing?.plan && team.billing.status === 'trialing')
|
||||
)
|
||||
.map((team) => team.billing?.plan);
|
||||
return plans.includes('enterprise')
|
||||
? 'enterprise'
|
||||
: plans.includes('pro')
|
||||
? 'pro'
|
||||
: 'hobby';
|
||||
}
|
||||
8
lib/session/get-server-session.ts
Normal file
8
lib/session/get-server-session.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { cookies } from 'next/headers';
|
||||
import { userSessionCookieName } from './constants';
|
||||
import { getSessionFromCookie } from './server';
|
||||
|
||||
export async function getServerSession() {
|
||||
const cookieValue = cookies().get(userSessionCookieName)?.value;
|
||||
return getSessionFromCookie(cookieValue);
|
||||
}
|
||||
23
lib/session/server.ts
Normal file
23
lib/session/server.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { type NextApiRequest } from 'next';
|
||||
import { type NextRequest } from 'next/server';
|
||||
import { userSessionCookieName } from './constants';
|
||||
import { decryptJWECookie } from '../cookies/decrypt-jwe';
|
||||
import { type Session } from './types';
|
||||
|
||||
export async function getSessionFromCookie(cookieValue?: string) {
|
||||
if (!cookieValue) return;
|
||||
|
||||
const session = await decryptJWECookie<Session>(cookieValue);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function getSessionFromReq(
|
||||
req: NextApiRequest | NextRequest
|
||||
): Promise<Session | undefined> {
|
||||
const cookieValue =
|
||||
'get' in req.cookies && typeof req.cookies.get === 'function'
|
||||
? req.cookies.get(userSessionCookieName)?.value
|
||||
: (req.cookies as Record<string, string>)[userSessionCookieName];
|
||||
return getSessionFromCookie(cookieValue);
|
||||
}
|
||||
13
lib/session/types.ts
Normal file
13
lib/session/types.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { BillingPlan } from './create';
|
||||
|
||||
export interface Session {
|
||||
vercelToken: string;
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
name?: string;
|
||||
plan: BillingPlan;
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue