diff --git a/lexicons/com/atproto/admin/defs.json b/lexicons/com/atproto/admin/defs.json index 318d1c33b5a..0d57da18ae8 100644 --- a/lexicons/com/atproto/admin/defs.json +++ b/lexicons/com/atproto/admin/defs.json @@ -248,7 +248,8 @@ } }, "invitesDisabled": { "type": "boolean" }, - "inviteNote": { "type": "string" } + "inviteNote": { "type": "string" }, + "emailConfirmedAt": { "type": "string", "format": "datetime" } } }, "accountView": { @@ -271,6 +272,7 @@ } }, "invitesDisabled": { "type": "boolean" }, + "emailConfirmedAt": { "type": "string", "format": "datetime" }, "inviteNote": { "type": "string" } } }, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index f17885819a0..404526e7936 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -434,6 +434,10 @@ export const schemaDict = { inviteNote: { type: 'string', }, + emailConfirmedAt: { + type: 'string', + format: 'datetime', + }, }, }, accountView: { @@ -469,6 +473,10 @@ export const schemaDict = { invitesDisabled: { type: 'boolean', }, + emailConfirmedAt: { + type: 'string', + format: 'datetime', + }, inviteNote: { type: 'string', }, diff --git a/packages/api/src/client/types/com/atproto/admin/defs.ts b/packages/api/src/client/types/com/atproto/admin/defs.ts index 7c48fa87a3c..5ab3e3482ec 100644 --- a/packages/api/src/client/types/com/atproto/admin/defs.ts +++ b/packages/api/src/client/types/com/atproto/admin/defs.ts @@ -241,6 +241,7 @@ export interface RepoViewDetail { invites?: ComAtprotoServerDefs.InviteCode[] invitesDisabled?: boolean inviteNote?: string + emailConfirmedAt?: string [k: string]: unknown } @@ -264,6 +265,7 @@ export interface AccountView { invitedBy?: ComAtprotoServerDefs.InviteCode invites?: ComAtprotoServerDefs.InviteCode[] invitesDisabled?: boolean + emailConfirmedAt?: string inviteNote?: string [k: string]: unknown } diff --git a/packages/bsky/src/api/com/atproto/admin/util.ts b/packages/bsky/src/api/com/atproto/admin/util.ts index 6217e71eb0f..7dfd10cce5c 100644 --- a/packages/bsky/src/api/com/atproto/admin/util.ts +++ b/packages/bsky/src/api/com/atproto/admin/util.ts @@ -32,6 +32,7 @@ export const addAccountInfoToRepoViewDetail = ( invitesDisabled: accountInfo.invitesDisabled, inviteNote: accountInfo.inviteNote, invites: accountInfo.invites, + emailConfirmedAt: accountInfo.emailConfirmedAt, } } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index f17885819a0..404526e7936 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -434,6 +434,10 @@ export const schemaDict = { inviteNote: { type: 'string', }, + emailConfirmedAt: { + type: 'string', + format: 'datetime', + }, }, }, accountView: { @@ -469,6 +473,10 @@ export const schemaDict = { invitesDisabled: { type: 'boolean', }, + emailConfirmedAt: { + type: 'string', + format: 'datetime', + }, inviteNote: { type: 'string', }, diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts index ea463368f8e..7c74f6b8b98 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts @@ -241,6 +241,7 @@ export interface RepoViewDetail { invites?: ComAtprotoServerDefs.InviteCode[] invitesDisabled?: boolean inviteNote?: string + emailConfirmedAt?: string [k: string]: unknown } @@ -264,6 +265,7 @@ export interface AccountView { invitedBy?: ComAtprotoServerDefs.InviteCode invites?: ComAtprotoServerDefs.InviteCode[] invitesDisabled?: boolean + emailConfirmedAt?: string inviteNote?: string [k: string]: unknown } diff --git a/packages/bsky/tests/admin/get-repo.test.ts b/packages/bsky/tests/admin/get-repo.test.ts index 3c1e909a4ab..9b4f6690ccd 100644 --- a/packages/bsky/tests/admin/get-repo.test.ts +++ b/packages/bsky/tests/admin/get-repo.test.ts @@ -91,6 +91,39 @@ describe('admin get repo view', () => { expect(triage).toEqual({ ...admin, email: undefined }) }) + it('includes emailConfirmedAt timestamp', async () => { + const { data: beforeEmailVerification } = + await agent.api.com.atproto.admin.getRepo( + { did: sc.dids.bob }, + { headers: network.pds.adminAuthHeaders() }, + ) + + expect(beforeEmailVerification.emailConfirmedAt).toBeUndefined() + const timestampBeforeVerification = Date.now() + const bobsAccount = sc.accounts[sc.dids.bob] + const verificationToken = await network.pds.ctx.services + .account(network.pds.ctx.db) + .createEmailToken(sc.dids.bob, 'confirm_email') + await agent.api.com.atproto.server.confirmEmail( + { email: bobsAccount.email, token: verificationToken }, + { + encoding: 'application/json', + + headers: sc.getHeaders(sc.dids.bob), + }, + ) + const { data: afterEmailVerification } = + await agent.api.com.atproto.admin.getRepo( + { did: sc.dids.bob }, + { headers: network.pds.adminAuthHeaders() }, + ) + + expect(afterEmailVerification.emailConfirmedAt).toBeTruthy() + expect( + new Date(afterEmailVerification.emailConfirmedAt as string).getTime(), + ).toBeGreaterThan(timestampBeforeVerification) + }) + it('fails when repo does not exist.', async () => { const promise = agent.api.com.atproto.admin.getRepo( { did: 'did:plc:doesnotexist' }, diff --git a/packages/pds/src/api/com/atproto/admin/util.ts b/packages/pds/src/api/com/atproto/admin/util.ts index f8bab4460a5..ee862236b4f 100644 --- a/packages/pds/src/api/com/atproto/admin/util.ts +++ b/packages/pds/src/api/com/atproto/admin/util.ts @@ -1,8 +1,4 @@ import express from 'express' -import { - RepoView, - RepoViewDetail, -} from '../../../../lexicon/types/com/atproto/admin/defs' // Output designed to passed as second arg to AtpAgent methods. // The encoding field here is a quirk of the AtpAgent. @@ -26,16 +22,3 @@ export function authPassthru(req: express.Request, withEncoding?: boolean) { } } } - -// @NOTE mutates. -// merges-in details that the pds knows about the repo. -export function mergeRepoViewPdsDetails( - other: T, - pds: T, -) { - other.email ??= pds.email - other.invites ??= pds.invites - other.invitedBy ??= pds.invitedBy - other.invitesDisabled ??= pds.invitesDisabled - return other -} diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index f17885819a0..404526e7936 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -434,6 +434,10 @@ export const schemaDict = { inviteNote: { type: 'string', }, + emailConfirmedAt: { + type: 'string', + format: 'datetime', + }, }, }, accountView: { @@ -469,6 +473,10 @@ export const schemaDict = { invitesDisabled: { type: 'boolean', }, + emailConfirmedAt: { + type: 'string', + format: 'datetime', + }, inviteNote: { type: 'string', }, diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts b/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts index ea463368f8e..7c74f6b8b98 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts @@ -241,6 +241,7 @@ export interface RepoViewDetail { invites?: ComAtprotoServerDefs.InviteCode[] invitesDisabled?: boolean inviteNote?: string + emailConfirmedAt?: string [k: string]: unknown } @@ -264,6 +265,7 @@ export interface AccountView { invitedBy?: ComAtprotoServerDefs.InviteCode invites?: ComAtprotoServerDefs.InviteCode[] invitesDisabled?: boolean + emailConfirmedAt?: string inviteNote?: string [k: string]: unknown } diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index 73518199c32..3893be03209 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -387,6 +387,7 @@ export class AccountService { 'did_handle.did', 'did_handle.handle', 'user_account.email', + 'user_account.emailConfirmedAt', 'user_account.invitesDisabled', 'user_account.inviteNote', 'user_account.createdAt as indexedAt', @@ -405,6 +406,7 @@ export class AccountService { handle: account?.handle ?? INVALID_HANDLE, invitesDisabled: account.invitesDisabled === 1, inviteNote: account.inviteNote ?? undefined, + emailConfirmedAt: account.emailConfirmedAt ?? undefined, invites, invitedBy: invitedBy[did], }