From 5ee9491485a16133a89daa97024c5312c2816e88 Mon Sep 17 00:00:00 2001 From: joostdecock Date: Sat, 19 Aug 2023 18:01:51 +0200 Subject: [PATCH] feat(backend): Updates for account pages --- sites/backend/src/controllers/users.mjs | 12 +++ sites/backend/src/models/apikey.mjs | 33 +++++--- sites/backend/src/models/user.mjs | 79 +++++++++++++++++-- sites/backend/src/routes/users.mjs | 8 ++ sites/backend/src/utils/cloudflare-images.mjs | 5 +- 5 files changed, 118 insertions(+), 19 deletions(-) diff --git a/sites/backend/src/controllers/users.mjs b/sites/backend/src/controllers/users.mjs index 1b006f17576..02ead8b7d4d 100644 --- a/sites/backend/src/controllers/users.mjs +++ b/sites/backend/src/controllers/users.mjs @@ -117,6 +117,18 @@ UsersController.prototype.profile = async (req, res, tools) => { return User.sendResponse(res) } +/* + * Returns all user data + * + * See: https://freesewing.dev/reference/backend/api + */ +UsersController.prototype.allData = async (req, res, tools) => { + const User = new UserModel(tools) + await User.allData(req) + + return User.sendResponse(res) +} + /* * Checks whether a submitted username is available * diff --git a/sites/backend/src/models/apikey.mjs b/sites/backend/src/models/apikey.mjs index e9de37ac178..5d6c6fa7f65 100644 --- a/sites/backend/src/models/apikey.mjs +++ b/sites/backend/src/models/apikey.mjs @@ -9,15 +9,20 @@ import { decorateModel } from '../utils/model-decorator.mjs' * @param {tools} object - A set of tools loaded in src/index.js * @returns {ApikeyModel} object - The ApikeyModel */ -export function ApikeyModel(tools) { +export function ApikeyModel(tools, models) { /* * See utils/model-decorator.mjs for details */ - return decorateModel(this, tools, { - name: 'apikey', - encryptedFields: ['name'], - models: ['user'], - }) + return decorateModel( + this, + tools, + { + name: 'apikey', + encryptedFields: ['name'], + models: ['user'], + }, + models + ) } /* @@ -167,12 +172,18 @@ ApikeyModel.prototype.userApikeys = async function (uid) { /* * Keys are an array, remove sercrets with map() and decrypt prior to returning */ - return keys.map((key) => { - delete key.secret - key.name = this.decrypt(key.name) + return keys.map((key) => this.asKeyData(key)) +} - return key - }) +/* + * Takes non-instatiated key data and prepares it so it can be returned + */ +ApikeyModel.prototype.asKeyData = async function (key) { + delete key.secret + delete key.aud + key.name = this.decrypt(key.name) + + return key } /* diff --git a/sites/backend/src/models/user.mjs b/sites/backend/src/models/user.mjs index 96679aa4bca..a7dab792bee 100644 --- a/sites/backend/src/models/user.mjs +++ b/sites/backend/src/models/user.mjs @@ -51,6 +51,43 @@ UserModel.prototype.profile = async function ({ params }) { }) } +/* + * Loads a user from the database based on the where clause you pass it + * In addition prepares it for returning all account data + * This is guarded so it enforces access control and validates input + * This is an anonymous route returning limited info (profile data) + * + * @param {params} object - The request (URL) parameters + * @returns {UserModel} object - The UserModel + */ +UserModel.prototype.allData = async function ({ params }) { + /* + * Is id set? + */ + if (typeof params.id === 'undefined') return this.setResponse(403, 'idMissing') + + /* + * Try to find the record in the database + * Note that find checks lusername, ehash, and id but we + * pass it in the username value as that's what the login + * rout does + */ + await this.read( + { id: Number(params.id) }, + { apikeys: true, bookmarks: true, patterns: true, sets: true } + ) + + /* + * If it does not exist, return 404 + */ + if (!this.exists) return this.setResponse(404) + + return this.setResponse200({ + result: 'success', + data: this.asData(), + }) +} + /* * Loads a user from the database based on the where clause you pass it * In addition prepares it for returning the account data @@ -857,15 +894,21 @@ UserModel.prototype.guardedUpdate = async function ({ body, user }) { /* * Image (img) */ - if (typeof body.img === 'string') - data.img = await replaceImage({ + if (typeof body.img === 'string') { + const imgData = { id: `user-${this.record.ihash}`, metadata: { user: user.uid, ihash: this.record.ihash, }, - b64: body.img, - }) + } + /* + * Allow both a base64 encoded binary image or an URL + */ + if (body.img.slice(0, 4) === 'http') imgData.url = body.img + else imgData.b64 = body.img + data.img = await replaceImage(imgData) + } /* * Now update the database record @@ -1161,6 +1204,7 @@ UserModel.prototype.asProfile = function () { id: this.record.id, bio: this.clear.bio, img: this.clear.img, + ihash: this.record.ihash, patron: this.record.patron, role: this.record.role, username: this.record.username, @@ -1185,7 +1229,7 @@ UserModel.prototype.asAccount = function () { createdAt: this.record.createdAt, email: this.clear.email, data: this.clear.data, - ihash: this.ihash, + ihash: this.record.ihash, img: this.clear.img, imperial: this.record.imperial, initial: this.clear.initial, @@ -1208,6 +1252,31 @@ UserModel.prototype.asAccount = function () { } } +/* + * Returns all user data (that is not included in the account data) + * + * @return {account} object - The account data as a plain object + */ +UserModel.prototype.asData = function () { + /* + * Nothing to do here but construct the object to return + */ + return { + apikeys: this.record.apikeys + ? this.record.apikeys.map((key) => { + delete key.secret + delete key.aud + key.name = this.decrypt(key.name) + + return key + }) + : [], + bookmarks: this.record.bookmarks || [], + patterns: this.record.patterns || [], + sets: this.record.sets || [], + } +} + /* * Returns a list of records as search results * Typically used by admin search diff --git a/sites/backend/src/routes/users.mjs b/sites/backend/src/routes/users.mjs index 2f3c73c517d..b4f8e6e5842 100644 --- a/sites/backend/src/routes/users.mjs +++ b/sites/backend/src/routes/users.mjs @@ -55,6 +55,14 @@ export function usersRoutes(tools) { Users.isUsernameAvailable(req, res, tools) ) + // Load full user data + app.get('/users/:id/jwt', passport.authenticate(...jwt), (req, res) => + Users.allData(req, res, tools) + ) + app.get('/users/:id/key', passport.authenticate(...bsc), (req, res) => + Users.allData(req, res, tools) + ) + // Load a user profile app.get('/users/:id', (req, res) => Users.profile(req, res, tools)) diff --git a/sites/backend/src/utils/cloudflare-images.mjs b/sites/backend/src/utils/cloudflare-images.mjs index 5110bfe7975..7515efca7bd 100644 --- a/sites/backend/src/utils/cloudflare-images.mjs +++ b/sites/backend/src/utils/cloudflare-images.mjs @@ -48,7 +48,7 @@ export async function replaceImage(props, isTest = false) { const form = getFormData(props) // Ignore errors on delete, probably means the image does not exist try { - await axios.delete(`${config.api}/${props.id}`) + await axios.delete(`${config.api}/${props.id}`, { headers }) } catch (err) { // It's fine log.info(`Could not delete image ${props.id}`) @@ -58,10 +58,9 @@ export async function replaceImage(props, isTest = false) { result = await axios.post(config.api, form, { headers }) } catch (err) { console.log('Failed to replace image on cloudflare', err) - console.log(err.response.data) } - return result.data?.result?.id ? result.data.result.id : false + return result?.data?.result?.id ? result.data.result.id : false } /*