build ok,
This commit is contained in:
13
002_source/cms/src/lib/auth/auth0/server.ts
Normal file
13
002_source/cms/src/lib/auth/auth0/server.ts
Normal 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!,
|
||||
});
|
15
002_source/cms/src/lib/auth/cognito/client.ts
Normal file
15
002_source/cms/src/lib/auth/cognito/client.ts
Normal 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!,
|
||||
},
|
||||
},
|
||||
});
|
98
002_source/cms/src/lib/auth/custom/client.ts
Normal file
98
002_source/cms/src/lib/auth/custom/client.ts
Normal 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();
|
10
002_source/cms/src/lib/auth/firebase/client.ts
Normal file
10
002_source/cms/src/lib/auth/firebase/client.ts
Normal 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());
|
||||
}
|
7
002_source/cms/src/lib/auth/strategy.ts
Normal file
7
002_source/cms/src/lib/auth/strategy.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const AuthStrategy = {
|
||||
CUSTOM: 'CUSTOM',
|
||||
AUTH0: 'AUTH0',
|
||||
COGNITO: 'COGNITO',
|
||||
FIREBASE: 'FIREBASE',
|
||||
SUPABASE: 'SUPABASE',
|
||||
} as const;
|
9
002_source/cms/src/lib/auth/supabase/metadata.d.ts
vendored
Normal file
9
002_source/cms/src/lib/auth/supabase/metadata.d.ts
vendored
Normal 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;
|
||||
}
|
31
002_source/cms/src/lib/auth/supabase/middleware.ts
Normal file
31
002_source/cms/src/lib/auth/supabase/middleware.ts
Normal 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`);
|
||||
}
|
26
002_source/cms/src/lib/dayjs.ts
Normal file
26
002_source/cms/src/lib/dayjs.ts
Normal 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 };
|
4
002_source/cms/src/lib/default-logger.ts
Normal file
4
002_source/cms/src/lib/default-logger.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { config } from '@/config';
|
||||
import { createLogger } from '@/lib/logger';
|
||||
|
||||
export const logger = createLogger({ level: config.logLevel });
|
26
002_source/cms/src/lib/firebase/client.ts
Normal file
26
002_source/cms/src/lib/firebase/client.ts
Normal 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;
|
||||
}
|
11
002_source/cms/src/lib/get-site-url.ts
Normal file
11
002_source/cms/src/lib/get-site-url.ts
Normal 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;
|
||||
}
|
12
002_source/cms/src/lib/i18n.ts
Normal file
12
002_source/cms/src/lib/i18n.ts
Normal 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);
|
||||
});
|
27
002_source/cms/src/lib/is-nav-item-active.ts
Normal file
27
002_source/cms/src/lib/is-nav-item-active.ts
Normal 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;
|
||||
}
|
71
002_source/cms/src/lib/logger.ts
Normal file
71
002_source/cms/src/lib/logger.ts
Normal 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 });
|
||||
}
|
13
002_source/cms/src/lib/settings/apply-default-settings.ts
Normal file
13
002_source/cms/src/lib/settings/apply-default-settings.ts
Normal 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,
|
||||
};
|
||||
}
|
29
002_source/cms/src/lib/settings/get-settings.ts
Normal file
29
002_source/cms/src/lib/settings/get-settings.ts
Normal 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;
|
||||
}
|
16
002_source/cms/src/lib/settings/set-settings.ts
Normal file
16
002_source/cms/src/lib/settings/set-settings.ts
Normal 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));
|
||||
}
|
8
002_source/cms/src/lib/supabase/client.ts
Normal file
8
002_source/cms/src/lib/supabase/client.ts
Normal 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!);
|
||||
}
|
36
002_source/cms/src/lib/supabase/middleware.ts
Normal file
36
002_source/cms/src/lib/supabase/middleware.ts
Normal 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 };
|
||||
}
|
36
002_source/cms/src/lib/supabase/server.ts
Normal file
36
002_source/cms/src/lib/supabase/server.ts
Normal 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.
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user