@ -11,21 +11,21 @@ export type EmployeeRole = 'owner' | 'manager' | 'staff'
const ROLES : readonly EmployeeRole [ ] = [ 'owner' , 'manager' , 'staff' ] as const
export interface Employee {
id : string ; email: string ; displayName : string ; role : EmployeeRole ; active : boolean
id : string ; username: string ; email: string ; displayName : string ; role : EmployeeRole ; active : boolean
phone : string | null ; title : string | null
}
interface EmployeeRow {
id : string ; email: string ; display_name : string ; role : string ; active : number
id : string ; username: string | null ; email: string ; display_name : string ; role : string ; active : number
phone : string | null ; title : string | null
}
// Explicit column list — pw_salt/pw_hash must never be selected here.
const COLS = 'id, email, display_name, role, active, phone, title'
const COLS = 'id, username, email, display_name, role, active, phone, title'
function toEmployee ( r : EmployeeRow ) : Employee {
return {
id : r.id , email: r.email , displayName : r.display_name ,
id : r.id , username: r.username ? ? r . email , email: r.email , displayName : r.display_name ,
role : r.role as EmployeeRole , active : r.active === 1 ,
phone : r.phone , title : r.title ,
}
@ -61,25 +61,30 @@ async function otherActiveOwnerExists(db: DB, excludeId: string): Promise<boolea
export async function createEmployee ( db : DB , userId : string , input : {
email : string ; displayName : string ; role : EmployeeRole ; password : string
username? : string // D25: login identifier; defaults to email when not given
} ) : Promise < Employee > {
assertRole ( input . role )
if ( input . password . length < 8 ) throw new Error ( 'Password must be at least 8 characters' )
const email = input . email . trim ( ) . toLowerCase ( )
if ( email === '' ) throw new Error ( 'Email is required' )
if ( input . displayName . trim ( ) === '' ) throw new Error ( 'Name is required' )
const username = ( input . username ? ? input . email ) . trim ( ) . toLowerCase ( )
if ( username === '' ) throw new Error ( 'Username is required' )
const id = uuidv7 ( )
const { salt , hash } = hashPin ( input . password ) // generic scrypt; PIN digit policy not applied
return db . transaction ( async ( ) = > {
// Pre-check the UNIQUE(email) so the caller gets a friendly, engine-neutral
// message instead of raw driver text (SQLite and Postgres word it differently).
// Friendly, engine-neutral pre-checks (login is by username; email stays a field).
if ( await db . get ( ` SELECT 1 FROM staff_user WHERE username=? ` , username ) !== undefined ) {
throw new Error ( 'Username already in use' )
}
if ( await db . get ( ` SELECT 1 FROM staff_user WHERE email=? ` , email ) !== undefined ) {
throw new Error ( 'Email already in use' )
}
await db . run (
` INSERT INTO staff_user (id, email, display_name, role, pw_salt, pw_hash) VALUES (?, ?, ?, ?, ?, ?)` ,
id , email, input . displayName , input . role , salt , hash ,
` INSERT INTO staff_user (id, username, email, display_name, role, pw_salt, pw_hash) VALUES (?, ?, ?, ?, ?, ?, ?)` ,
id , username, email, input . displayName , input . role , salt , hash ,
)
await writeAudit ( db , userId , 'create' , 'staff_user' , id , undefined , { email, role : input.role } )
await writeAudit ( db , userId , 'create' , 'staff_user' , id , undefined , { username, email, role : input.role } )
return ( await getEmployee ( db , id ) ) !
} )
}