From d098e26f82096188a8ef910561c5ebc7a784a399 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 21 Jan 2022 11:20:39 -0500 Subject: [PATCH] User Email and email validation API (#10160) * feat: wip user emails api * fix: allow admins with manage-users access to email confirmation api as well * fix: wrong route path * docs: openapi spec --- public/openapi/write.yaml | 6 +++ public/openapi/write/users/uid/emails.yaml | 33 +++++++++++++ .../openapi/write/users/uid/emails/email.yaml | 25 ++++++++++ .../write/users/uid/emails/email/confirm.yaml | 34 +++++++++++++ src/controllers/write/users.js | 48 +++++++++++++++++++ src/routes/write/users.js | 4 ++ 6 files changed, 150 insertions(+) create mode 100644 public/openapi/write/users/uid/emails.yaml create mode 100644 public/openapi/write/users/uid/emails/email.yaml create mode 100644 public/openapi/write/users/uid/emails/email/confirm.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 2e1eb4f9e8..1e73eb4cf9 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -78,6 +78,12 @@ paths: $ref: 'write/users/uid/invites.yaml' /users/{uid}/invites/groups: $ref: 'write/users/uid/invites/groups.yaml' + /users/{uid}/emails: + $ref: 'write/users/uid/emails.yaml' + /users/{uid}/emails/{email}: + $ref: 'write/users/uid/emails/email.yaml' + /users/{uid}/emails/{email}/confirm: + $ref: 'write/users/uid/emails/email/confirm.yaml' /groups/: $ref: 'write/groups.yaml' /groups/{slug}: diff --git a/public/openapi/write/users/uid/emails.yaml b/public/openapi/write/users/uid/emails.yaml new file mode 100644 index 0000000000..8bd00809c0 --- /dev/null +++ b/public/openapi/write/users/uid/emails.yaml @@ -0,0 +1,33 @@ +get: + tags: + - users + summary: get user emails + description: | + This operation lists all emails associated with the user. + This route is accessible to all users if the target user has elected to show their email publicly. Otherwise, it is only accessible to privileged users, or if the calling user is the same as the target user. + parameters: + - in: path + required: true + name: uid + schema: + type: number + description: A valid user id + example: 1 + responses: + '200': + description: user emails successfully listed + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + emails: + type: array + items: + type: string + description: An email address \ No newline at end of file diff --git a/public/openapi/write/users/uid/emails/email.yaml b/public/openapi/write/users/uid/emails/email.yaml new file mode 100644 index 0000000000..7f0118db5a --- /dev/null +++ b/public/openapi/write/users/uid/emails/email.yaml @@ -0,0 +1,25 @@ +get: + tags: + - users + summary: get user's email data + description: | + This operation lists the data associated with a single email. + This route is accessible to all users if the target user has elected to show their email publicly. Otherwise, it is only accessible to privileged users, or if the calling user is the same as the target user. + parameters: + - in: path + required: true + name: uid + schema: + type: number + description: A valid user id + example: 1 + - in: path + required: true + name: email + schema: + type: string + description: A valid email address + example: test@example.org + responses: + '204': + description: user's email data successfully retrieved \ No newline at end of file diff --git a/public/openapi/write/users/uid/emails/email/confirm.yaml b/public/openapi/write/users/uid/emails/email/confirm.yaml new file mode 100644 index 0000000000..6ccd34ff51 --- /dev/null +++ b/public/openapi/write/users/uid/emails/email/confirm.yaml @@ -0,0 +1,34 @@ +post: + tags: + - users + summary: validate a user's email address + description: | + Marks the passed-in user's email as confirmed. + This route is only accessible to administrators with the `admin:users` privilege (or superadmins) + parameters: + - in: path + required: true + name: uid + schema: + type: number + description: A valid user id + example: 1 + - in: path + required: true + name: email + schema: + type: string + description: A valid email address + example: test@example.org + responses: + '200': + description: successfully confirmed a user email + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../../components/schemas/Status.yaml#/Status + response: + type: object \ No newline at end of file diff --git a/src/controllers/write/users.js b/src/controllers/write/users.js index 2469ae4b2a..5edad764fe 100644 --- a/src/controllers/write/users.js +++ b/src/controllers/write/users.js @@ -233,3 +233,51 @@ Users.getInviteGroups = async function (req, res) { const userInviteGroups = await groups.getUserInviteGroups(req.params.uid); return helpers.formatApiResponse(200, res, userInviteGroups); }; + +Users.listEmails = async (req, res) => { + const [isPrivileged, { showemail }] = await Promise.all([ + user.isPrivileged(req.uid), + user.getSettings(req.params.uid), + ]); + const isSelf = req.uid === parseInt(req.params.uid, 10); + + if (isSelf || isPrivileged || showemail) { + const emails = await db.getSortedSetRangeByScore('email:uid', 0, 500, req.params.uid, req.params.uid); + helpers.formatApiResponse(200, res, { emails }); + } else { + helpers.formatApiResponse(204, res); + } +}; + +Users.getEmail = async (req, res) => { + const [isPrivileged, { showemail }, exists] = await Promise.all([ + user.isPrivileged(req.uid), + user.getSettings(req.params.uid), + db.isSortedSetMember('email:uid', req.params.email.toLowerCase()), + ]); + const isSelf = req.uid === parseInt(req.params.uid, 10); + + if (exists && (isSelf || isPrivileged || showemail)) { + helpers.formatApiResponse(204, res); + } else { + helpers.formatApiResponse(404, res); + } +}; + +Users.confirmEmail = async (req, res) => { + const [exists, canManage] = await Promise.all([ + db.isSortedSetMember('email:uid', req.params.email.toLowerCase()), + privileges.admin.can('admin:users', req.uid), + ]); + + if (!canManage) { + helpers.notAllowed(req, res); + } + + if (exists) { + await user.email.confirmByUid(req.params.uid); + helpers.formatApiResponse(200, res); + } else { + helpers.formatApiResponse(404, res); + } +}; diff --git a/src/routes/write/users.js b/src/routes/write/users.js index 418fa9897d..d3d3b0017d 100644 --- a/src/routes/write/users.js +++ b/src/routes/write/users.js @@ -44,6 +44,10 @@ function authenticatedRoutes() { setupApiRoute(router, 'post', '/:uid/invites', middlewares, controllers.write.users.invite); setupApiRoute(router, 'get', '/:uid/invites/groups', [...middlewares, middleware.assert.user], controllers.write.users.getInviteGroups); + setupApiRoute(router, 'get', '/:uid/emails', [...middlewares, middleware.assert.user], controllers.write.users.listEmails); + setupApiRoute(router, 'get', '/:uid/emails/:email', [...middlewares, middleware.assert.user], controllers.write.users.getEmail); + setupApiRoute(router, 'post', '/:uid/emails/:email/confirm', [...middlewares, middleware.assert.user], controllers.write.users.confirmEmail); + // Shorthand route to access user routes by userslug router.all('/+bySlug/:userslug*?', [], controllers.write.users.redirectBySlug); }