build ok,

This commit is contained in:
louiscklaw
2025-04-14 09:26:24 +08:00
commit 6c931c1fe8
770 changed files with 63959 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
import { initAuth0 } from '@auth0/nextjs-auth0';
import { config } from '@/config';
// Read the notes from https://auth0.github.io/nextjs-auth0/types/config.ConfigParameters.html
export const auth0 = initAuth0({
secret: config.auth0.secret!,
baseURL: config.auth0.baseUrl!,
issuerBaseURL: config.auth0.issuerBaseUrl!,
clientID: config.auth0.clientId!,
clientSecret: config.auth0.clientSecret!,
});

View File

@@ -0,0 +1,15 @@
'use client';
import { Amplify } from 'aws-amplify';
import { config } from '@/config';
Amplify.configure({
Auth: {
Cognito: {
identityPoolId: config.cognito.identityPoolId!,
userPoolClientId: config.cognito.userPoolClientId!,
userPoolId: config.cognito.userPoolId!,
},
},
});

View File

@@ -0,0 +1,98 @@
'use client';
import type { User } from '@/types/user';
function generateToken(): string {
const arr = new Uint8Array(12);
window.crypto.getRandomValues(arr);
return Array.from(arr, (v) => v.toString(16).padStart(2, '0')).join('');
}
const user = {
id: 'USR-000',
avatar: '/assets/avatar.png',
firstName: 'Sofia',
lastName: 'Rivers',
email: 'sofia@devias.io',
} satisfies User;
export interface SignUpParams {
firstName: string;
lastName: string;
email: string;
password: string;
}
export interface SignInWithOAuthParams {
provider: 'google' | 'discord';
}
export interface SignInWithPasswordParams {
email: string;
password: string;
}
export interface ResetPasswordParams {
email: string;
}
class AuthClient {
async signUp(_: SignUpParams): Promise<{ error?: string }> {
// Make API request
// We do not handle the API, so we'll just generate a token and store it in localStorage.
const token = generateToken();
localStorage.setItem('custom-auth-token', token);
return {};
}
async signInWithOAuth(_: SignInWithOAuthParams): Promise<{ error?: string }> {
return { error: 'Social authentication not implemented' };
}
async signInWithPassword(params: SignInWithPasswordParams): Promise<{ error?: string }> {
const { email, password } = params;
// Make API request
// We do not handle the API, so we'll check if the credentials match with the hardcoded ones.
if (email !== 'sofia@devias.io' || password !== 'Secret1') {
return { error: 'Invalid credentials' };
}
const token = generateToken();
localStorage.setItem('custom-auth-token', token);
return {};
}
async resetPassword(_: ResetPasswordParams): Promise<{ error?: string }> {
return { error: 'Password reset not implemented' };
}
async updatePassword(_: ResetPasswordParams): Promise<{ error?: string }> {
return { error: 'Update reset not implemented' };
}
async getUser(): Promise<{ data?: User | null; error?: string }> {
// Make API request
// We do not handle the API, so just check if we have a token in localStorage.
const token = localStorage.getItem('custom-auth-token');
if (!token) {
return { data: null };
}
return { data: user };
}
async signOut(): Promise<{ error?: string }> {
localStorage.removeItem('custom-auth-token');
return {};
}
}
export const authClient = new AuthClient();

View File

@@ -0,0 +1,10 @@
'use client';
import type { Auth } from 'firebase/auth';
import { getAuth } from 'firebase/auth';
import { getFirebaseApp } from '@/lib/firebase/client';
export function getFirebaseAuth(): Auth {
return getAuth(getFirebaseApp());
}

View File

@@ -0,0 +1,7 @@
export const AuthStrategy = {
CUSTOM: 'CUSTOM',
AUTH0: 'AUTH0',
COGNITO: 'COGNITO',
FIREBASE: 'FIREBASE',
SUPABASE: 'SUPABASE',
} as const;

View File

@@ -0,0 +1,9 @@
// NOTE: You can define the user.user_metadata here
// But I recommend to override the type exposed by supabase-js library
// using TS Module Override strategy
export interface UserMetadata {
avatar: string | undefined;
first_name: string | undefined;
last_name: string | undefined;
}

View File

@@ -0,0 +1,31 @@
import type { NextRequest, NextResponse } from 'next/server';
import { logger } from '@/lib/default-logger';
import { createClient } from '@/lib/supabase/middleware';
export async function supabaseMiddleware(req: NextRequest): Promise<NextResponse> {
const { supabaseClient, res } = createClient(req);
try {
const { error } = await supabaseClient.auth.getSession();
if (error) {
logger.debug('Something went wrong, deleted auth token cookie');
removeAuthToken(res);
}
} catch (err) {
logger.debug('Something went wrong, deleted auth token cookie');
removeAuthToken(res);
throw err;
}
return res;
}
// Supabase does not automatically remove the auth token in
// case of token expiration, invalidity, etc.
// If an error is thrown, we assume that the token is no longer valid
// and we remove it. Thus the user will have to reauthenticate.
function removeAuthToken(res: NextResponse): void {
res.cookies.delete(`sb-${process.env.NEXT_PUBLIC_SUPABASE_REF_ID}-auth-token`);
}

View File

@@ -0,0 +1,26 @@
import dayjs, { extend } from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import updateLocale from 'dayjs/plugin/updateLocale';
extend(relativeTime);
extend(updateLocale);
dayjs.updateLocale('en', {
relativeTime: {
future: 'in %s',
past: '%s ago',
s: 'a few sec',
m: 'a min',
mm: '%d min',
h: 'an hour',
hh: '%d hours',
d: 'a day',
dd: '%d days',
M: 'a month',
MM: '%d months',
y: 'a year',
yy: '%d years',
},
});
export { dayjs };

View File

@@ -0,0 +1,4 @@
import { config } from '@/config';
import { createLogger } from '@/lib/logger';
export const logger = createLogger({ level: config.logLevel });

View File

@@ -0,0 +1,26 @@
'use client';
import { initializeApp } from 'firebase/app';
import type { FirebaseApp } from 'firebase/app';
import { config } from '@/config';
// This executes on the client only, so we can cache the app instance.
let appInstance: FirebaseApp;
export function getFirebaseApp(): FirebaseApp {
if (appInstance) {
return appInstance;
}
appInstance = initializeApp({
apiKey: config.firebase.apiKey,
authDomain: config.firebase.authDomain,
projectId: config.firebase.projectId,
storageBucket: config.firebase.storageBucket,
messagingSenderId: config.firebase.messagingSenderId,
appId: config.firebase.appId,
});
return appInstance;
}

View File

@@ -0,0 +1,11 @@
export function getSiteURL(): string {
let url =
process.env.NEXT_PUBLIC_SITE_URL ?? // Set this to your site URL in production env.
process.env.NEXT_PUBLIC_VERCEL_URL ?? // Automatically set by Vercel.
'http://localhost:3000/';
// Make sure to include `https://` when not localhost.
url = url.includes('http') ? url : `https://${url}`;
// Make sure to include a trailing `/`.
url = url.endsWith('/') ? url : `${url}/`;
return url;
}

View File

@@ -0,0 +1,12 @@
import { use } from 'i18next';
import Backend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';
import { logger } from '@/lib/default-logger';
export const i18n = use(Backend)
.use(initReactI18next)
.init({ lng: 'en', fallbackLng: 'en', interpolation: { escapeValue: false } })
.catch((err) => {
logger.error('Failed to initialize i18n', err);
});

View File

@@ -0,0 +1,27 @@
import type { NavItemConfig } from '@/types/nav';
export function isNavItemActive({
disabled,
external,
href,
matcher,
pathname,
}: Pick<NavItemConfig, 'disabled' | 'external' | 'href' | 'matcher'> & { pathname: string }): boolean {
if (disabled || !href || external) {
return false;
}
if (matcher) {
if (matcher.type === 'startsWith') {
return pathname.startsWith(matcher.href);
}
if (matcher.type === 'equals') {
return pathname === matcher.href;
}
return false;
}
return pathname === href;
}

View File

@@ -0,0 +1,71 @@
/* eslint-disable no-console -- Allow */
// NOTE: A tracking system such as Sentry should replace the console
export const LogLevel = { NONE: 'NONE', ERROR: 'ERROR', WARN: 'WARN', DEBUG: 'DEBUG', ALL: 'ALL' } as const;
const LogLevelNumber = { NONE: 0, ERROR: 1, WARN: 2, DEBUG: 3, ALL: 4 } as const;
export interface LoggerOptions {
prefix?: string;
level?: keyof typeof LogLevel;
showLevel?: boolean;
}
export class Logger {
protected prefix: string;
protected level: keyof typeof LogLevel;
protected showLevel: boolean;
private levelNumber: number;
constructor({ prefix = '', level = LogLevel.ALL, showLevel = true }: LoggerOptions) {
this.prefix = prefix;
this.level = level;
this.levelNumber = LogLevelNumber[this.level];
this.showLevel = showLevel;
}
debug = (...args: unknown[]): void => {
if (this.canWrite(LogLevel.DEBUG)) {
this.write(LogLevel.DEBUG, ...args);
}
};
warn = (...args: unknown[]): void => {
if (this.canWrite(LogLevel.WARN)) {
this.write(LogLevel.WARN, ...args);
}
};
error = (...args: unknown[]): void => {
if (this.canWrite(LogLevel.ERROR)) {
this.write(LogLevel.ERROR, ...args);
}
};
private canWrite(level: keyof typeof LogLevel): boolean {
return this.levelNumber >= LogLevelNumber[level];
}
private write(level: keyof typeof LogLevel, ...args: unknown[]): void {
let prefix = this.prefix;
if (this.showLevel) {
prefix = `- ${level} ${prefix}`;
}
if (level === LogLevel.ERROR) {
console.error(prefix, ...args);
} else {
console.log(prefix, ...args);
}
}
}
// This can be extended to create context specific logger (Server Action, Router Handler, etc.)
// to add context information (IP, User-Agent, timestamp, etc.)
export function createLogger({ prefix, level }: LoggerOptions = {}): Logger {
return new Logger({ prefix, level });
}

View File

@@ -0,0 +1,13 @@
import type { Settings } from '@/types/settings';
import { config } from '@/config';
export function applyDefaultSettings(settings: Partial<Settings>): Settings {
return {
colorScheme: config.site.colorScheme,
primaryColor: config.site.primaryColor,
direction: 'ltr',
navColor: 'evident',
layout: 'vertical',
...settings,
};
}

View File

@@ -0,0 +1,29 @@
'use server';
import { cookies } from 'next/headers';
import type { Settings } from '@/types/settings';
import { logger } from '@/lib/default-logger';
/**
* Retrieve the settings from client's cookies.
* This should be used in Server Components.
*/
export async function getSettings(): Promise<Partial<Settings>> {
const cookieStore = cookies();
const settingsStr = cookieStore.get('app.settings')?.value;
let settings: Partial<Settings>;
if (settingsStr) {
try {
settings = JSON.parse(settingsStr) as Partial<Settings>;
} catch {
logger.error('Unable to parse the settings');
}
}
settings ||= {};
return settings;
}

View File

@@ -0,0 +1,16 @@
'use server';
import { cookies } from 'next/headers';
import type { Settings } from '@/types/settings';
/**
* Store settings (partial patch) in client's cookies.
* This should be used as Server Action.
*
* To remove a specific key, set its value to `null`.
*/
export async function setSettings(settings: Partial<Settings>): Promise<void> {
const cookieStore = cookies();
cookieStore.set('app.settings', JSON.stringify(settings));
}

View File

@@ -0,0 +1,8 @@
import { createBrowserClient } from '@supabase/ssr';
import type { SupabaseClient } from '@supabase/supabase-js';
import { config } from '@/config';
export function createClient(): SupabaseClient {
return createBrowserClient(config.supabase.url!, config.supabase.anonKey!);
}

View File

@@ -0,0 +1,36 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import type { CookieOptions } from '@supabase/ssr';
import { createServerClient } from '@supabase/ssr';
import type { SupabaseClient } from '@supabase/supabase-js';
import { config } from '@/config';
type ResponseCookie = Pick<CookieOptions, 'httpOnly' | 'maxAge' | 'priority'>;
export function createClient(req: NextRequest): { supabaseClient: SupabaseClient; res: NextResponse } {
// Create an unmodified response
let res = NextResponse.next({ request: { headers: req.headers } });
const supabaseClient = createServerClient(config.supabase.url!, config.supabase.anonKey!, {
cookies: {
get(name: string) {
return req.cookies.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
// If the cookie is updated, update the cookies for the request and response
req.cookies.set({ name, value, ...(options as Partial<ResponseCookie>) });
res = NextResponse.next({ request: { headers: req.headers } });
res.cookies.set({ name, value, ...(options as Partial<ResponseCookie>) });
},
remove(name: string, options: CookieOptions) {
// If the cookie is removed, update the cookies for the request and response
req.cookies.set({ name, value: '', ...(options as Partial<ResponseCookie>) });
res = NextResponse.next({ request: { headers: req.headers } });
res.cookies.set({ name, value: '', ...(options as Partial<ResponseCookie>) });
},
},
});
return { supabaseClient, res };
}

View File

@@ -0,0 +1,36 @@
import type { cookies } from 'next/headers';
import type { CookieOptions } from '@supabase/ssr';
import { createServerClient } from '@supabase/ssr';
import type { SupabaseClient } from '@supabase/supabase-js';
import { config } from '@/config';
type ResponseCookie = Pick<CookieOptions, 'httpOnly' | 'maxAge' | 'priority'>;
export function createClient(cookieStore: ReturnType<typeof cookies>): SupabaseClient {
return createServerClient(config.supabase.url!, config.supabase.anonKey!, {
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...(options as Partial<ResponseCookie>) });
} catch (error) {
// The `set` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: '', ...(options as Partial<ResponseCookie>) });
} catch (error) {
// The `delete` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
});
}