Skip to content

Commit

Permalink
User avatar / roles return values
Browse files Browse the repository at this point in the history
  • Loading branch information
goto-bus-stop committed Aug 28, 2024
1 parent 22c0119 commit 89ffc17
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 31 deletions.
2 changes: 1 addition & 1 deletion src/controllers/authenticate.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ async function register(req) {
password,
});

return toItemResponse(user);
return toItemResponse(serializeUser(user));
} catch (error) {
throw beautifyDuplicateKeyError(error);
}
Expand Down
1 change: 1 addition & 0 deletions src/migrations/002-sql.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ async function up({ context: uw }) {
.addColumn('email', 'text')
.addColumn('password', 'text')
.addColumn('slug', 'text', (col) => col.notNull().unique())
.addColumn('avatar', 'text')
.addColumn('pending_activation', 'boolean', (col) => col.defaultTo(null))
.addColumn('created_at', 'timestamp', (col) => col.notNull().defaultTo(now))
.addColumn('updated_at', 'timestamp', (col) => col.notNull().defaultTo(now))
Expand Down
83 changes: 53 additions & 30 deletions src/plugins/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import bcrypt from 'bcryptjs';
import Page from '../Page.js';
import { IncorrectPasswordError, UserNotFoundError } from '../errors/index.js';
import { slugify } from 'transliteration';
import { jsonGroupArray } from '../utils/sqlite.js';
import { sql } from 'kysely';
import { randomUUID } from 'crypto';

const { pick, omit } = lodash;

Expand All @@ -18,6 +21,29 @@ function encryptPassword(password) {
return bcrypt.hash(password, 10);
}

/** @param {import('kysely').ExpressionBuilder<import('../schema.js').Database, 'users'>} eb */
const userRolesColumn = (eb) => eb.selectFrom('userRoles')
.where('userRoles.userID', '=', eb.ref('users.id'))
.select((sb) => jsonGroupArray(sb.ref('userRoles.role')).as('roles'));
/** @param {import('kysely').ExpressionBuilder<import('../schema.js').Database, 'users'>} eb */
const avatarColumn = (eb) => eb.fn.coalesce(
'users.avatar',
/** @type {import('kysely').RawBuilder<string>} */ (sql`concat('https://sigil.u-wave.net/', ${eb.ref('users.id')})`),
);

/** @type {import('kysely').SelectExpression<import('../schema.js').Database, 'users'>[]} */
const userSelection = [
'users.id',
'users.username',
'users.slug',
'users.activePlaylistID',
'users.pendingActivation',
'users.createdAt',
'users.updatedAt',
(eb) => avatarColumn(eb).as('avatar'),
(eb) => userRolesColumn(eb).as('roles'),
]

class UsersRepository {
#uw;

Expand Down Expand Up @@ -56,7 +82,7 @@ class UsersRepository {
const query = baseQuery
.offset(offset)
.limit(limit)
.select(['id', 'username', 'slug', 'activePlaylistID', 'pendingActivation', 'createdAt', 'updatedAt']);
.select(userSelection);

const [
users,
Expand Down Expand Up @@ -90,10 +116,15 @@ class UsersRepository {

const user = await db.selectFrom('users')
.where('id', '=', id)
.select(['id', 'username', 'slug', 'activePlaylistID', 'pendingActivation', 'createdAt', 'updatedAt'])
.select(userSelection)
.executeTakeFirst();

return user ? { ...user, roles: [] } : null;
if (user == null) {
return null;
}

const roles = /** @type {string[]} */ (JSON.parse(/** @type {string} */ (/** @type {unknown} */ (user.roles))));
return Object.assign(user, { roles });
}

/**
Expand Down Expand Up @@ -125,15 +156,7 @@ class UsersRepository {
async localLogin({ email, password }) {
const user = await this.#uw.db.selectFrom('users')
.where('email', '=', email)
.select([
'id',
'username',
'password',
'slug',
'pendingActivation',
'createdAt',
'updatedAt',
])
.select(userSelection)
.executeTakeFirst();
if (!user) {
throw new UserNotFoundError({ email });
Expand Down Expand Up @@ -196,13 +219,7 @@ class UsersRepository {
'authServices.service',
'authServices.serviceID',
'authServices.serviceAvatar',
'users.id',
'users.username',
'users.slug',
'users.activePlaylistID',
'users.pendingActivation',
'users.createdAt',
'users.updatedAt',
...userSelection,
])
.executeTakeFirst();

Expand All @@ -215,10 +232,11 @@ class UsersRepository {
} else {
const user = await tx.insertInto('users')
.values({
id: /** @type {UserID} */ (randomUUID()),
username: username ? username.replace(/\s/g, '') : `${type}.${id}`,
slug: slugify(username),
pendingActivation: true, // type,
// avatar,
avatar,
})
.returningAll()
.executeTakeFirstOrThrow();
Expand Down Expand Up @@ -251,37 +269,42 @@ class UsersRepository {
async createUser({
username, email, password,
}) {
const { db } = this.#uw;
const { acl, db } = this.#uw;

this.#logger.info({ username, email: email.toLowerCase() }, 'create user');

const hash = await encryptPassword(password);

const user = await db.insertInto('users')
.values({
id: /** @type {UserID} */ (randomUUID()),
username,
email,
password: hash,
slug: slugify(username),
pendingActivation: false,
pendingActivation: /** @type {boolean} */ (/** @type {unknown} */ (0)),
})
.returning([
'id',
'username',
'slug',
'activePlaylistID',
'pendingActivation',
'createdAt',
'updatedAt',
'users.id',
'users.username',
'users.slug',
(eb) => avatarColumn(eb).as('avatar'),
'users.activePlaylistID',
'users.pendingActivation',
'users.createdAt',
'users.updatedAt',
])
.executeTakeFirstOrThrow();

const roles = ['user'];
await acl.allow(user, roles);

this.#uw.publish('user:create', {
user: user.id,
auth: { type: 'local', email: email.toLowerCase() },
});

return user;
return Object.assign(user, { roles });
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/utils/sqlite.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,14 @@ export function arrayCycle(expr) {
`
}

/**
* @template {unknown} T
* @param {import('kysely').Expression<T>} expr
* @returns {import('kysely').RawBuilder<T[]>}
*/
export function jsonGroupArray(expr) {
return sql`json_group_array(${expr})`;
}

/** @type {import('kysely').RawBuilder<Date>} */
export const now = sql`(strftime('%FT%T', 'now'))`;

0 comments on commit 89ffc17

Please sign in to comment.