2026-07-13 03:48:45 +00:00
|
|
|
import fs from 'fs';
|
|
|
|
|
import path from 'path';
|
|
|
|
|
import { randomUUID } from 'crypto';
|
|
|
|
|
|
|
|
|
|
const DATA_DIR = path.join( __dirname, '..', '..', 'build', 'data' );
|
|
|
|
|
if ( !fs.existsSync( DATA_DIR ) ) fs.mkdirSync( DATA_DIR, { recursive: true } );
|
|
|
|
|
|
|
|
|
|
export interface UserProduct
|
|
|
|
|
{
|
|
|
|
|
id: string;
|
|
|
|
|
acquiredAt: string;
|
|
|
|
|
source: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface User
|
|
|
|
|
{
|
|
|
|
|
id: string;
|
|
|
|
|
email: string;
|
|
|
|
|
passwordHash: string;
|
|
|
|
|
roles: string[];
|
|
|
|
|
products: UserProduct[];
|
|
|
|
|
settings: Record<string, unknown>;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RefreshToken
|
|
|
|
|
{
|
|
|
|
|
token: string;
|
|
|
|
|
userId: string;
|
|
|
|
|
expiresAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ResetToken
|
|
|
|
|
{
|
|
|
|
|
token: string;
|
|
|
|
|
userId: string;
|
|
|
|
|
expiresAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function read<T>( table: string ): T[]
|
|
|
|
|
{
|
|
|
|
|
const file = path.join( DATA_DIR, `${table}.json` );
|
|
|
|
|
if ( !fs.existsSync( file ) ) return [];
|
|
|
|
|
return JSON.parse( fs.readFileSync( file, 'utf8' ) ) as T[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function write<T>( table: string, rows: T[] ): void
|
|
|
|
|
{
|
|
|
|
|
fs.writeFileSync( path.join( DATA_DIR, `${table}.json` ), JSON.stringify( rows, null, 2 ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const users =
|
|
|
|
|
{
|
|
|
|
|
all: (): User[] => read<User>( 'users' ),
|
|
|
|
|
|
|
|
|
|
findById: ( id: string ): User | undefined =>
|
|
|
|
|
read<User>( 'users' ).find( u => u.id === id ),
|
|
|
|
|
|
|
|
|
|
findByEmail: ( email: string ): User | undefined =>
|
|
|
|
|
read<User>( 'users' ).find( u => u.email.toLowerCase() === email.toLowerCase() ),
|
|
|
|
|
|
|
|
|
|
create( email: string, passwordHash: string ): User
|
|
|
|
|
{
|
|
|
|
|
const rows = read<User>( 'users' );
|
|
|
|
|
if ( rows.find( u => u.email.toLowerCase() === email.toLowerCase() ) ) throw new Error( 'Taken' );
|
|
|
|
|
const user: User =
|
|
|
|
|
{
|
|
|
|
|
id: randomUUID(),
|
|
|
|
|
email,
|
|
|
|
|
passwordHash,
|
|
|
|
|
roles: [ 'user' ],
|
|
|
|
|
products: [],
|
|
|
|
|
settings: {},
|
|
|
|
|
createdAt: new Date().toISOString()
|
|
|
|
|
};
|
|
|
|
|
write( 'users', [ ...rows, user ] );
|
|
|
|
|
return user;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
update( id: string, patch: Partial<Omit<User, 'id' | 'createdAt'>> ): void
|
|
|
|
|
{
|
|
|
|
|
const rows = read<User>( 'users' ).map( u => u.id === id ? { ...u, ...patch } : u );
|
|
|
|
|
write( 'users', rows );
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
delete( id: string ): void
|
|
|
|
|
{
|
|
|
|
|
write( 'users', read<User>( 'users' ).filter( u => u.id !== id ) );
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const refreshTokens =
|
|
|
|
|
{
|
|
|
|
|
find: ( token: string ): RefreshToken | undefined =>
|
|
|
|
|
read<RefreshToken>( 'refreshTokens' ).find( t => t.token === token ),
|
|
|
|
|
|
2026-07-17 18:34:36 +00:00
|
|
|
create( userId: string, ttlMs: number = 30 * 24 * 60 * 60 * 1000 ): RefreshToken
|
2026-07-13 03:48:45 +00:00
|
|
|
{
|
|
|
|
|
const token: RefreshToken =
|
|
|
|
|
{
|
|
|
|
|
token: randomUUID(),
|
|
|
|
|
userId,
|
2026-07-17 18:34:36 +00:00
|
|
|
expiresAt: new Date( Date.now() + ttlMs ).toISOString()
|
2026-07-13 03:48:45 +00:00
|
|
|
};
|
2026-07-17 18:34:36 +00:00
|
|
|
|
2026-07-13 03:48:45 +00:00
|
|
|
write( 'refreshTokens', [ ...read<RefreshToken>( 'refreshTokens' ), token ] );
|
2026-07-17 18:34:36 +00:00
|
|
|
|
2026-07-13 03:48:45 +00:00
|
|
|
return token;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
delete( token: string ): void
|
|
|
|
|
{
|
|
|
|
|
write( 'refreshTokens', read<RefreshToken>( 'refreshTokens' ).filter( t => t.token !== token ) );
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
deleteForUser( userId: string ): void
|
|
|
|
|
{
|
|
|
|
|
write( 'refreshTokens', read<RefreshToken>( 'refreshTokens' ).filter( t => t.userId !== userId ) );
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const resetTokens =
|
|
|
|
|
{
|
|
|
|
|
find: ( token: string ): ResetToken | undefined =>
|
|
|
|
|
read<ResetToken>( 'resetTokens' ).find( t => t.token === token ),
|
|
|
|
|
|
|
|
|
|
create( userId: string ): ResetToken
|
|
|
|
|
{
|
|
|
|
|
const token: ResetToken =
|
|
|
|
|
{
|
|
|
|
|
token: randomUUID(),
|
|
|
|
|
userId,
|
|
|
|
|
expiresAt: new Date( Date.now() + 60 * 60 * 1000 ).toISOString()
|
|
|
|
|
};
|
|
|
|
|
const existing = read<ResetToken>( 'resetTokens' ).filter( t => t.userId !== userId );
|
|
|
|
|
write( 'resetTokens', [ ...existing, token ] );
|
|
|
|
|
return token;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
delete( token: string ): void
|
|
|
|
|
{
|
|
|
|
|
write( 'resetTokens', read<ResetToken>( 'resetTokens' ).filter( t => t.token !== token ) );
|
2026-07-13 10:33:39 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
deleteForUser( userId: string ): void
|
|
|
|
|
{
|
|
|
|
|
write( 'resetTokens', read<ResetToken>( 'resetTokens' ).filter( t => t.userId !== userId ) );
|
2026-07-13 03:48:45 +00:00
|
|
|
}
|
|
|
|
|
};
|