From d8b50c73e4915277da049e18cd5f6296d978aff4 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren <dtholmgren@gmail.com> Date: Thu, 13 Apr 2023 14:43:53 -0500 Subject: [PATCH] Add admin.updateAccountEamil (#812) * -add admin capability to update account email * pr feedback --- .../com/atproto/admin/updateAccountEmail.json | 25 +++++++++++++ packages/api/src/client/index.ts | 13 +++++++ packages/api/src/client/lexicons.ts | 28 ++++++++++++++ .../com/atproto/admin/updateAccountEmail.ts | 34 +++++++++++++++++ .../pds/src/api/com/atproto/admin/index.ts | 2 + .../com/atproto/admin/updateAccountEmail.ts | 21 +++++++++++ packages/pds/src/lexicon/index.ts | 11 ++++++ packages/pds/src/lexicon/lexicons.ts | 28 ++++++++++++++ .../com/atproto/admin/updateAccountEmail.ts | 37 +++++++++++++++++++ packages/pds/src/services/account/index.ts | 8 ++++ packages/pds/tests/account.test.ts | 30 +++++++++++++++ 11 files changed, 237 insertions(+) create mode 100644 lexicons/com/atproto/admin/updateAccountEmail.json create mode 100644 packages/api/src/client/types/com/atproto/admin/updateAccountEmail.ts create mode 100644 packages/pds/src/api/com/atproto/admin/updateAccountEmail.ts create mode 100644 packages/pds/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts diff --git a/lexicons/com/atproto/admin/updateAccountEmail.json b/lexicons/com/atproto/admin/updateAccountEmail.json new file mode 100644 index 00000000..98a66e84 --- /dev/null +++ b/lexicons/com/atproto/admin/updateAccountEmail.json @@ -0,0 +1,25 @@ +{ + "lexicon": 1, + "id": "com.atproto.admin.updateAccountEmail", + "defs": { + "main": { + "type": "procedure", + "description": "Administrative action to update an account's email", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["account", "email"], + "properties": { + "account": { + "type": "string", + "format": "at-identifier", + "description": "The handle or DID of the repo." + }, + "email": {"type": "string"} + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 246d0c7b..84194368 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -20,6 +20,7 @@ import * as ComAtprotoAdminResolveModerationReports from './types/com/atproto/ad import * as ComAtprotoAdminReverseModerationAction from './types/com/atproto/admin/reverseModerationAction' import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRepos' import * as ComAtprotoAdminTakeModerationAction from './types/com/atproto/admin/takeModerationAction' +import * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' import * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' import * as ComAtprotoIdentityResolveHandle from './types/com/atproto/identity/resolveHandle' import * as ComAtprotoIdentityUpdateHandle from './types/com/atproto/identity/updateHandle' @@ -108,6 +109,7 @@ export * as ComAtprotoAdminResolveModerationReports from './types/com/atproto/ad export * as ComAtprotoAdminReverseModerationAction from './types/com/atproto/admin/reverseModerationAction' export * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRepos' export * as ComAtprotoAdminTakeModerationAction from './types/com/atproto/admin/takeModerationAction' +export * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' export * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' export * as ComAtprotoIdentityResolveHandle from './types/com/atproto/identity/resolveHandle' export * as ComAtprotoIdentityUpdateHandle from './types/com/atproto/identity/updateHandle' @@ -398,6 +400,17 @@ export class AdminNS { }) } + updateAccountEmail( + data?: ComAtprotoAdminUpdateAccountEmail.InputSchema, + opts?: ComAtprotoAdminUpdateAccountEmail.CallOptions, + ): Promise<ComAtprotoAdminUpdateAccountEmail.Response> { + return this._service.xrpc + .call('com.atproto.admin.updateAccountEmail', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoAdminUpdateAccountEmail.toKnownErr(e) + }) + } + updateAccountHandle( data?: ComAtprotoAdminUpdateAccountHandle.InputSchema, opts?: ComAtprotoAdminUpdateAccountHandle.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 231cfb0e..76c912cf 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -1063,6 +1063,33 @@ export const schemaDict = { }, }, }, + ComAtprotoAdminUpdateAccountEmail: { + lexicon: 1, + id: 'com.atproto.admin.updateAccountEmail', + defs: { + main: { + type: 'procedure', + description: "Administrative action to update an account's email", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['account', 'email'], + properties: { + account: { + type: 'string', + format: 'at-identifier', + description: 'The handle or DID of the repo.', + }, + email: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, ComAtprotoAdminUpdateAccountHandle: { lexicon: 1, id: 'com.atproto.admin.updateAccountHandle', @@ -4796,6 +4823,7 @@ export const ids = { 'com.atproto.admin.reverseModerationAction', ComAtprotoAdminSearchRepos: 'com.atproto.admin.searchRepos', ComAtprotoAdminTakeModerationAction: 'com.atproto.admin.takeModerationAction', + ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail', ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle', ComAtprotoIdentityResolveHandle: 'com.atproto.identity.resolveHandle', ComAtprotoIdentityUpdateHandle: 'com.atproto.identity.updateHandle', diff --git a/packages/api/src/client/types/com/atproto/admin/updateAccountEmail.ts b/packages/api/src/client/types/com/atproto/admin/updateAccountEmail.ts new file mode 100644 index 00000000..f023c9ae --- /dev/null +++ b/packages/api/src/client/types/com/atproto/admin/updateAccountEmail.ts @@ -0,0 +1,34 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export interface InputSchema { + /** The handle or DID of the repo. */ + account: string + email: string + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers + qp?: QueryParams + encoding: 'application/json' +} + +export interface Response { + success: boolean + headers: Headers +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/pds/src/api/com/atproto/admin/index.ts b/packages/pds/src/api/com/atproto/admin/index.ts index fefe8d26..26ccf8c4 100644 --- a/packages/pds/src/api/com/atproto/admin/index.ts +++ b/packages/pds/src/api/com/atproto/admin/index.ts @@ -13,6 +13,7 @@ import getModerationReports from './getModerationReports' import disableInviteCodes from './disableInviteCodes' import getInviteCodes from './getInviteCodes' import updateAccountHandle from './updateAccountHandle' +import updateAccountEmail from './updateAccountEmail' export default function (server: Server, ctx: AppContext) { resolveModerationReports(server, ctx) @@ -28,4 +29,5 @@ export default function (server: Server, ctx: AppContext) { disableInviteCodes(server, ctx) getInviteCodes(server, ctx) updateAccountHandle(server, ctx) + updateAccountEmail(server, ctx) } diff --git a/packages/pds/src/api/com/atproto/admin/updateAccountEmail.ts b/packages/pds/src/api/com/atproto/admin/updateAccountEmail.ts new file mode 100644 index 00000000..ad759473 --- /dev/null +++ b/packages/pds/src/api/com/atproto/admin/updateAccountEmail.ts @@ -0,0 +1,21 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.admin.updateAccountEmail({ + auth: ctx.adminVerifier, + handler: async ({ input }) => { + await ctx.db.transaction(async (dbTxn) => { + const accntService = ctx.services.account(dbTxn) + const account = await accntService.getAccount(input.body.account) + if (!account) { + throw new InvalidRequestError( + `Account does not exist: ${input.body.account}`, + ) + } + await accntService.updateEmail(account.did, input.body.email) + }) + }, + }) +} diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 2820f92b..4e3667c1 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -21,6 +21,7 @@ import * as ComAtprotoAdminResolveModerationReports from './types/com/atproto/ad import * as ComAtprotoAdminReverseModerationAction from './types/com/atproto/admin/reverseModerationAction' import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRepos' import * as ComAtprotoAdminTakeModerationAction from './types/com/atproto/admin/takeModerationAction' +import * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' import * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' import * as ComAtprotoIdentityResolveHandle from './types/com/atproto/identity/resolveHandle' import * as ComAtprotoIdentityUpdateHandle from './types/com/atproto/identity/updateHandle' @@ -257,6 +258,16 @@ export class AdminNS { return this._server.xrpc.method(nsid, cfg) } + updateAccountEmail<AV extends AuthVerifier>( + cfg: ConfigOf< + AV, + ComAtprotoAdminUpdateAccountEmail.Handler<ExtractAuth<AV>> + >, + ) { + const nsid = 'com.atproto.admin.updateAccountEmail' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + updateAccountHandle<AV extends AuthVerifier>( cfg: ConfigOf< AV, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 231cfb0e..76c912cf 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -1063,6 +1063,33 @@ export const schemaDict = { }, }, }, + ComAtprotoAdminUpdateAccountEmail: { + lexicon: 1, + id: 'com.atproto.admin.updateAccountEmail', + defs: { + main: { + type: 'procedure', + description: "Administrative action to update an account's email", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['account', 'email'], + properties: { + account: { + type: 'string', + format: 'at-identifier', + description: 'The handle or DID of the repo.', + }, + email: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, ComAtprotoAdminUpdateAccountHandle: { lexicon: 1, id: 'com.atproto.admin.updateAccountHandle', @@ -4796,6 +4823,7 @@ export const ids = { 'com.atproto.admin.reverseModerationAction', ComAtprotoAdminSearchRepos: 'com.atproto.admin.searchRepos', ComAtprotoAdminTakeModerationAction: 'com.atproto.admin.takeModerationAction', + ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail', ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle', ComAtprotoIdentityResolveHandle: 'com.atproto.identity.resolveHandle', ComAtprotoIdentityUpdateHandle: 'com.atproto.identity.updateHandle', diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts b/packages/pds/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts new file mode 100644 index 00000000..eb00af00 --- /dev/null +++ b/packages/pds/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts @@ -0,0 +1,37 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + /** The handle or DID of the repo. */ + account: string + email: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type Handler<HA extends HandlerAuth = never> = (ctx: { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +}) => Promise<HandlerOutput> | HandlerOutput diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index c0c57e17..1f7f429c 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -145,6 +145,14 @@ export class AccountService { await sequenceHandleUpdate(this.db, did, handle) } + async updateEmail(did: string, email: string) { + await this.db.db + .updateTable('user_account') + .set({ email: email.toLowerCase() }) + .where('did', '=', did) + .executeTakeFirst() + } + async updateUserPassword(did: string, password: string) { const passwordScrypt = await scrypt.hash(password) await this.db.db diff --git a/packages/pds/tests/account.test.ts b/packages/pds/tests/account.test.ts index d3b8f8f0..135b2582 100644 --- a/packages/pds/tests/account.test.ts +++ b/packages/pds/tests/account.test.ts @@ -168,6 +168,36 @@ describe('account', () => { ]) }) + it('allows administrative email updates', async () => { + await agent.api.com.atproto.admin.updateAccountEmail( + { + account: handle, + email: 'alIce-NEw@teST.com', + }, + { + encoding: 'application/json', + headers: { authorization: util.adminAuth() }, + }, + ) + + const accnt = await ctx.services.account(ctx.db).getAccount(handle) + expect(accnt?.email).toBe('alice-new@test.com') + + await agent.api.com.atproto.admin.updateAccountEmail( + { + account: did, + email, + }, + { + encoding: 'application/json', + headers: { authorization: util.adminAuth() }, + }, + ) + + const accnt2 = await ctx.services.account(ctx.db).getAccount(handle) + expect(accnt2?.email).toBe(email) + }) + it('disallows duplicate email addresses and handles', async () => { const inviteCode = await createInviteCode(agent, 2) const email = 'bob@test.com'