All files / src ctx.ts

84.4% Statements 249/295
83.33% Branches 20/24
45% Functions 9/20
84.4% Lines 249/295

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 2951x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 17x 35x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 35x 35x 17x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x 35x                   35x 35x 35x 35x 35x 35x 35x 17x 17x 17x 17x 17x 17x 17x 35x       35x       35x 17x 17x 17x 35x 35x 35x 35x 35x             35x     35x 35x                         35x     35x     35x 35x     35x 17x 17x 17x 35x 35x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 34x 34x 17x 17x 17x 17x 17x     17x 35x 35x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 96821x 96821x 96821x 96821x 96821x 3193899x 3193899x 3097113x 3097113x 3000295x 3193899x 96821x 96821x 1x 1x 1x 1x 1x 1x 1x       1x 1x 1x 1x 18x 18x 18x 1x 1x 1x
 
 
import { getMainConfig } from './helpers/getGreenDotConfigs'
import { ApiOutputTypes } from './types/core.types'
import mongoose from 'mongoose'
import { Request, Response } from 'express'
import { env } from './helpers/getEnv'
 
import { getId } from 'topkat-utils'
import { ThrowErrorTypeSafe, errorWithCtx } from './error'
import { dbs } from './db'
import { type ModelTypes } from './cache/dbs/index.generated'
 
import { banUser, addUserWarning } from './security/userAndConnexion/banAndAddUserWarning'
 
//----------------------------------------
// CTX CLASS
//----------------------------------------
/** `ctx` stores contextual informations about a request like user permissions, paginationData...etc
 * * ctx is scoped to a request and is carried along during all the request lifetime in the backend
 * * That's why `ctx` is used everywhere as the first parameter of 99.9% backend functions
 * */
export class CtxClass {
    /** TODO not actually working Number; 1 or 2 => verbosity */
    debugMode = false
    /** dev, prod, preprod... */
    env = process.env.NODE_ENV
    /** used to cimple check if it's a ctx for sure and not another object */
    isCtx = true as const
    /** Public Ctx means user is not logged */
    isPublic = false
    /** SystemCtx is used by developper 🚸 to bypass all security 🚸 when making a request */
    isSystem = false
    /** Used when no db calls needs to be made but all the process is to be ran */
    simulateRequest = false
    /** Used to store */
    transactionSession?: mongoose.mongo.ClientSession
    /** actual userId or a public | system generic id */
    _id: string = publicUserId
    /** The actual user role as given by the JWT token */
    role: GD['role'] | TechnicalRoles = 'public'
    /** All valid authentication methods used by the user */
    authenticationMethod: Array<AuthenticationMethod | 'apiKey' | 'accessToken'> = []
    /** Used to define through witch platform the ctx is connected, to be overrided by app */
    platform!: string
    /** user stored in the cache */
    _user?: ModelTypes['user']
    /** The actual user permissions fields as given by the JWT token */
    permissions: UserPermissionFields
    /** This is to store the type that will be used in all the for clauses in the app, since in a for you have to provide role. This is not the ideal place to put it, toDo */
    permissionsWithoutRolePermissions!: UserPermissionsWithoutRolePerms
    /** Api request informations */
    api: {
        params: Record<string, any>
        body: Record<string, any>
        originalUrl: string
        query: Record<string, any>
        ipAdress?: string
        req: Request
        res: Response
        /** This is to configure the output type of the api request. Default: json */
        outputType?: ((req: Record<string, any>, ctx: Ctx) => ApiOutputTypes) | ApiOutputTypes
    }
    GM!: typeof this
    /** */
    isFromGeneratedDbApi = false as boolean
    //----------------------------------------
    // CONSTRUCTOR
    //----------------------------------------
    /** NOTE: req object is modified by the constructor */
    constructor(ctx: Ctx)
    constructor(ctxUser: CtxUser, req?: Request, res?: Response, previousCtx?: Ctx)
    constructor(
        ctxUser: CtxUser | Ctx,
        req: Request = {} as Request,
        res: Response = {} as Response,
        previousCtx?: Ctx,
    ) {
 
        if ('isCtx' in ctxUser) {
            Object.assign(this, ctxUser)
        } else {
            if (previousCtx) Object.assign(this, previousCtx)
 
            this.api = {
                params: req?.params || {},
                body: req?.body || {},
                originalUrl: req?.originalUrl,
                query: req?.query || {},
                ipAdress: req?.ip,
                req,
                res,
            }
 
            const { _id, ...restOfCtxUser } = ctxUser
 
            this._id = _id.toString()
 
            Object.assign(this, restOfCtxUser)
        }
 
        if (this.role === 'public') this.isPublic = true
        else if (this.role === 'system') this.isSystem = true
 
        // TODO THIS SHOULD BE ASYNC CODE with await
        // TODO type override make "this" type not to work
        // events.emit('ctx.creation', this as any)
 
        return withProxy(this)
    }
    /** Cleanly throw an error, associating all ctx infos to it (user._id, aplication, service name, route...) */
    error = new Proxy(
        {} as ThrowErrorTypeSafe,
        {
            /** This will inject Ctx (this) as first param of error */
            get: (_, p) => {
                const errorFn = errorWithCtx[p as string]
                if (typeof errorFn === 'function') {
                    return (...args) => { // arrow function here are the trick for keeping this in that context
                        return errorFn.apply(errorWithCtx, [
                            this,
                            ...args
                        ])
                    }
                } else return errorWithCtx[p as string] // here all not existing errors are handled by error proxy
            },
        })
    //----------------------------------------
    // METHODS
    //----------------------------------------
    /** Use that to change the role used by the actual Ctx, other user permission related fields may be changed at the same time, that's why there is the param fieldsToMergeWithCtxUser */
    useRole(
        role: Ctx['role'],
        permissionsOrUser: Partial<typeof this['permissions']> = {},
        /** default: true; If false, will return the created ctx without modifying the actual one */
        modifyActualCtx = true
    ) {
        return this.fromUser(role, permissionsOrUser, modifyActualCtx)
    }
    async addWarning() {
        const discriminator = this.isSystem || this.isPublic ? this.api.ipAdress : this._id
        return await addUserWarning(this, { discriminator })
    }
    async banUser() {
        const discriminator = this.isSystem || this.isPublic ? this.api.ipAdress : this._id
        return await banUser(this, { discriminator })
    }
    system() {
        if (this.isSystem) return this
        else return this.clone({ ...this, isSystem: true as const, isPublic: false as const, role: 'system' as const })
    }
    /** Check if user has this role
     * system will always return true or false depending on
     * systemAlwaysReturnTrue value (default, false)
     */
    hasRole(
        role: Ctx['role'],
        systemAlwaysReturnTrue = false
    ) {
        if (this.isSystem) return systemAlwaysReturnTrue
        else return this.role === role
    }
    toString() {
        return JSON.stringify(this, null, 2)
    }
    /** Returns the ctx user like it is in database with the permission 'system' (⚠️ with all fields including password and sensitive fields ⚠️) */
    async getUser({
        refreshCache = false,
        errorIfNotSet = true
    } = {}): Promise<ModelTypes['user']> {

        const { defaultDatabaseName } = getMainConfig()

        if (refreshCache === false && this._user) {
            return this._user
        } else {
            return await dbs[defaultDatabaseName].user.getById(this.system(), this._id, { triggerErrorIfNotSet: errorIfNotSet })
        }
    }
    getUserMinimal() {
        return { _id: this._id, role: this.role, premissions: this.permissions }
    }
    clearUserCache() {
        delete this._user
    }
    /** This is to check if the user Id a real logged userId or a generated one corresponding to public or system */
    isAnonymousUser() {
        return !this._id || isAnonymousUser(this._id)
    }
    clone<T extends Record<string, any>>(override: T = {} as any): Ctx & T {
        const newCtx = new CtxClass({ ...this, ...override } as any as Ctx)
        return newCtx as Ctx & T
    }
    /** Build and return or modify Ctx from user fields and role */
    fromUser(role: Ctx['role'], user: Partial<ModelTypes['user']>, modifyActualCtx = true) {
 
        const { allPermissions } = getMainConfig()
 
        const newFields = {
            _id: getId(user),
            role,
            isSystem: false,
            isPublic: role === 'public',
            permissions: {} as any,
        } satisfies Partial<Ctx>
 
        for (const perm of allPermissions) {
            if (user?.[perm]) newFields[perm] = user[perm]
        }
 
        if (modifyActualCtx) {
            Object.assign(this, newFields)
            return this
        } else {
            return this.clone({ ...this, newFields })
        }
    }
 
}
 
 
 
//----------------------------------------
// TYPE GLOBAL
//----------------------------------------
 
declare global {
    interface Ctx extends CtxClass { }
    interface CtxUser {
        _id: Ctx['_id']
        role: Ctx['role']
        permissions: Partial<Ctx['permissions']>
        platform?: Ctx['platform']
        user?: Ctx['_user']
        authenticationMethod?: Ctx['authenticationMethod']
    }
    interface SystemCtx extends Ctx { isSystem: true, isPublic: false }
    interface PublicCtx extends Ctx { isSystem: false, isPublic: true }
}
 
//----------------------------------------
// CONSTANTS
//----------------------------------------
 
export const systemRole = 'system'
export type SystemRole = typeof systemRole
export const publicRole = 'public'
export type PublicRole = typeof publicRole
export const technicalRoles = [systemRole, publicRole] as const
export type TechnicalRoles = typeof technicalRoles[number]
export const systemUserId = '777fffffffffffffffffffff' // same are used in good-cop config
export const publicUserId = '000fffffffffffffffffffff'
 
export const authenticationMethod = ['biometricAuthToken', 'pincode', '2FA'] as const
export type AuthenticationMethod = typeof authenticationMethod[number]
 
//----------------------------------------
// HELPERS
//----------------------------------------
/** This is to check if the user Id a real logged userId or a generated one corresponding to public or system */
export const isAnonymousUser = id => [systemUserId, publicUserId].includes(id)
 
function withProxy<T extends CtxClass>(
    ctx: T
): T & { GM: T & { isSystem: true, isPublic: false } } {
    return new Proxy(ctx, {
        get(target, prop) {
            // allow to use more digest ctx.GM (God Mode) as an alias for ctx.system()
            if (prop === 'GM') return withProxy(ctx.isSystem ? ctx : ctx.system())
            // helps returning always last env
            else if (prop === 'env') return env.env
            else return target[prop]
        }
    }) as any
}
 
//----------------------------------------
// INSTANCIATED CTXs
//----------------------------------------
/**
 * Use it when you don't already have access to a ctx from which to do ctx.GM
 */
export function newSystemCtx() {
    return new CtxClass({ role: 'system', _id: systemUserId, permissions: {} }) as SystemCtx
}
 
/**
 * Use it when you want anonymous user to have a ctx
 */
export function newPublicCtx() {
    return new CtxClass({ role: 'public', _id: publicUserId, permissions: {} }) as PublicCtx
}
 
 
export const ctx = newPublicCtx() as Ctx