All files / realm/blong-access/adapter/db accessAccountAdd.ts

100% Statements 126/126
80% Branches 8/10
100% Functions 1/1
100% Lines 126/126

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 1271x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7x 7x 7x 7x 7x 7x 7x 7x 7x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 2x 2x 3x 3x 3x 3x 3x 3x 5x 5x 5x 5x 3x 3x 3x 5x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 5x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 1x  
import {type IMeta, handler} from '@feasibleone/blong';
 
import {type EnsuredRole} from './accessModel.ts';
import * as account from './account.ts';
import {type PasswordParams} from './password.ts';
 
type KnexQb = any;
 
/**
 * Low-level account creation for the access realm.
 *
 * Creates a `core_resource` (resourceName = email) + `access_user` row, one
 * credential (PBKDF2 `password` or `google` subject id), the `hasRole` Guest
 * edges, and refreshes the materialized `core_path`.  Enforces email
 * uniqueness.  Returns the new `userId` (hex UUID string).
 */
export default handler(
    ({
        errors,
        handler: {
            'db/accessRoleEnsure': accessRoleEnsure,
            'db/coreResourceEnsure': coreResourceEnsure,
            'db/coreTripleMerge': coreTripleMerge,
        },
        lib: {hashPassword, credentialPolicyParams},
    }) =>
        async function accessAccountAdd(
            params: {
                /** Resource name — the normalized email used as the login key. */
                name: string;
                /** Display email stored on `access_user` (defaults to `name`). */
                emailAddress?: string;
                /** Creates a PBKDF2 password credential when present. */
                password?: string;
                /** Creates a `google` credential storing the provider subject id. */
                googleSubjectId?: string;
                isActive?: boolean;
                /** Comma-separated role names to grant (e.g. `Guest`). */
                roles?: string;
            },
            $meta: IMeta,
        ): Promise<{userId: string}> {
            const qb: KnexQb = this.config?.context?.queryBuilder;
            if (!qb) throw new Error('Database not available');
 
            const email = params.emailAddress ?? params.name;
 
            // Uniqueness: no existing access.user resource with this resourceName
            const existing = await qb
                .select('core_resource.resourceId')
                .from('core_resource')
                .join('core_type', 'core_resource.typeId', 'core_type.typeId')
                .where('core_type.typeAlias', 'access.user')
                .where('core_resource.resourceName', params.name)
                .first();
            if (existing) {
                throw errors.errorAccountExists({params: {emailAddress: email}});
            }
 
            const {resourceId: userId} = await coreResourceEnsure<{resourceId: string}>(
                {
                    name: params.name,
                    typeAlias: 'access.user',
                    table: 'access_user',
                    extraColumns: {emailAddress: email, isActive: params.isActive ?? 1},
                    keyName: 'userId',
                },
                $meta,
            );
 
            // Credential — password (PBKDF2) or Google subject id
            if (params.password) {
                const salt = account.newUuid();
                // Credential params come from the active policy; config.password is the fallback.
                const policyParams = await credentialPolicyParams(qb, 'password');
                const {hash, params: credentialParams} = hashPassword<{
                    hash: string;
                    params: PasswordParams;
                }>(params.password, salt, policyParams);
                await qb('access_credential')
                    .insert({
                        userId: account.uuidBuf(userId),
                        credentialType: 'password',
                        credentialHash: hash,
                        credentialSalt: salt,
                        // `*JSON` column — the knex adapter stores this object as JSON.
                        credentialParamsJSON: credentialParams,
                        isActive: 1,
                    })
                    .onConflict()
                    .ignore();
            } else if (params.googleSubjectId) {
                await qb('access_credential')
                    .insert({
                        userId: account.uuidBuf(userId),
                        credentialType: 'google',
                        credentialHash: params.googleSubjectId,
                        credentialSalt: '',
                        isActive: 1,
                    })
                    .onConflict()
                    .ignore();
            }
 
            // Role edges (e.g. Guest) — batched via the shared `core.triple.merge`
            // helper (P3), which also refreshes `access_path` once.
            const triples: Array<{subjectId: string; predicateName: string; objectId: string}> = [];
            if (params.roles) {
                const roleNames = account.splitNames(params.roles);
                for (const roleName of roleNames) {
                    // Roles are never created here with a hardcoded bit any more:
                    // `access.role.ensure` allocates one when the role is new and
                    // returns the existing role — with its own bit — otherwise.
                    const {role} = await accessRoleEnsure<EnsuredRole>({role: {roleName}}, $meta);
                    triples.push({
                        subjectId: userId,
                        predicateName: 'hasRole',
                        objectId: role.roleId,
                    });
                }
            }
            await coreTripleMerge({triples, refreshPath: true}, $meta);
 
            return {userId};
        },
);