feature(auth): replace clerk.com with custom auth (#103)

* feature(auth): replace clerk.com with custom auth

* minor fixes

* remove notification preferences

* decrease live events interval

fix(api): cookies..

# Conflicts:
#	.gitignore
#	apps/api/src/index.ts
#	apps/dashboard/src/app/providers.tsx
#	packages/trpc/src/trpc.ts
This commit is contained in:
Carl-Gerhard Lindesvärd
2024-12-18 21:30:39 +01:00
committed by Carl-Gerhard Lindesvärd
parent f28802b1c2
commit d31d9924a5
151 changed files with 18484 additions and 12853 deletions

View File

@@ -0,0 +1,20 @@
import type { ISetCookie } from '@openpanel/validation';
import { COOKIE_OPTIONS } from '../constants';
export function setSessionTokenCookie(
setCookie: ISetCookie,
token: string,
expiresAt: Date,
): void {
setCookie('session', token, {
maxAge: expiresAt.getTime() - new Date().getTime(),
...COOKIE_OPTIONS,
});
}
export function deleteSessionTokenCookie(setCookie: ISetCookie): void {
setCookie('session', '', {
maxAge: 0,
...COOKIE_OPTIONS,
});
}

View File

@@ -0,0 +1,4 @@
export * from './cookie';
export * from './oauth';
export * from './password';
export * from './session';

View File

@@ -0,0 +1,18 @@
import { GitHub } from 'arctic';
export type { OAuth2Tokens } from 'arctic';
import * as Arctic from 'arctic';
export { Arctic };
export const github = new GitHub(
process.env.GITHUB_CLIENT_ID ?? '',
process.env.GITHUB_CLIENT_SECRET ?? '',
process.env.GITHUB_REDIRECT_URI ?? '',
);
export const google = new Arctic.Google(
process.env.GOOGLE_CLIENT_ID ?? '',
process.env.GOOGLE_CLIENT_SECRET ?? '',
process.env.GOOGLE_REDIRECT_URI ?? '',
);

View File

@@ -0,0 +1,41 @@
import { hash, verify } from '@node-rs/argon2';
import { sha1 } from '@oslojs/crypto/sha1';
import { encodeHexLowerCase } from '@oslojs/encoding';
export async function hashPassword(password: string): Promise<string> {
return await hash(password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
}
export async function verifyPasswordHash(
hash: string,
password: string,
): Promise<boolean> {
return await verify(hash, password);
}
export async function verifyPasswordStrength(
password: string,
): Promise<boolean> {
if (password.length < 8 || password.length > 255) {
return false;
}
const hash = encodeHexLowerCase(sha1(new TextEncoder().encode(password)));
const hashPrefix = hash.slice(0, 5);
const response = await fetch(
`https://api.pwnedpasswords.com/range/${hashPrefix}`,
);
const data = await response.text();
const items = data.split('\n');
for (const item of items) {
const hashSuffix = item.slice(0, 35).toLowerCase();
if (hash === hashPrefix + hashSuffix) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,83 @@
import crypto from 'node:crypto';
import { type Session, type User, db } from '@openpanel/db';
import { sha256 } from '@oslojs/crypto/sha2';
import {
encodeBase32LowerCaseNoPadding,
encodeHexLowerCase,
} from '@oslojs/encoding';
export function generateSessionToken(): string {
const bytes = new Uint8Array(20);
crypto.getRandomValues(bytes);
const token = encodeBase32LowerCaseNoPadding(bytes);
return token;
}
export async function createSession(
token: string,
userId: string,
): Promise<Session> {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session: Session = {
id: sessionId,
userId,
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
createdAt: new Date(),
updatedAt: new Date(),
};
await db.session.create({
data: session,
});
return session;
}
export const EMPTY_SESSION: SessionValidationResult = {
session: null,
user: null,
userId: null,
};
export async function validateSessionToken(
token: string | null,
): Promise<SessionValidationResult> {
if (!token) {
return EMPTY_SESSION;
}
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const result = await db.session.findUnique({
where: {
id: sessionId,
},
include: {
user: true,
},
});
if (result === null) {
return EMPTY_SESSION;
}
const { user, ...session } = result;
if (Date.now() >= session.expiresAt.getTime()) {
await db.session.delete({ where: { id: sessionId } });
return EMPTY_SESSION;
}
if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) {
session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30);
await db.session.update({
where: {
id: session.id,
},
data: {
expiresAt: session.expiresAt,
},
});
}
return { session, user, userId: user.id };
}
export async function invalidateSession(sessionId: string): Promise<void> {
await db.session.delete({ where: { id: sessionId } });
}
export type SessionValidationResult =
| { session: Session; user: User; userId: string }
| { session: null; user: null; userId: null };