Skip to content

Commit

Permalink
Account deletion (bluesky-social#488)
Browse files Browse the repository at this point in the history
* wip

* fleshing out repo storage

* fleshing out sql storage

* cleaning things up

* fix up tests

* dumb bug - commit log reversed

* rm staging in favor of commiting diffs to blockstore

* clean up benches

* fixing up sql storage

* some caching for sql repo store

* pr feedback

* migration

* wip

* migraiton test

* unclear param

* sql repo storage tests

* rm unused code

* fix up some diff code

* pr feedback

* enum for action types

* missed some

* wip

* ripping out auth lib

* more auth cleanup

* another lurker

* wip better sync primitives

* wip

* improving diffs & sync

* tests working!

* actually implemented checkout lol

* simplify interface & improve error handling

* writing sql storage code

* fixing up tests

* testing & bugfixes

* checkouts return records instead of cids

* one last refactor lol

* missed one

* handle other cid codecs on incoming car verification

* tests + tricky bugs

* unneeded blockstore method

* trim mst on del instead of save

* cleanup comment

* dont resolve did for every commit

* use "commit" instead of "root"

* getRoot -> getHead

* pr feedback

* very silly bug fix

* improve sync output

* reorging + sync of particular records

* serve & verify proofs. also rename some ipld methods

* fix up sync issue in mst

* find reachable records form carfile

* getRecord xrpc method

* pr feedback

* better migration test

* check migraiton result

* fixing up a couple things for pg

* explicit migrateTo

* async exceptions

* ipld car mimetype + remove updateRepo

* Update module publish scripts (bluesky-social#478)

* Update pds package publishing scripts

* Update auth package publishing scripts

* Update crypto package publishing scripts

* Update did-resolver package publishing scripts

* Update handle package publishing scripts

* Update xrpc-server package publishing scripts

* Update common package publishing scripts

* Update plc package publishing scripts

* Update uri package publishing scripts

* Update repo package publishing scripts

* Sort "suggested follows" by number of posts (bluesky-social#477)

* return suggestions by post count

* pr feedback

* fix up PG pagination issue

* partiion commit-history & commit-blocks by user did

* some lexicons

reworking routes

request deletion flows

delete actor rows

migration for user-partitioned-cids

move creator to be on ipld_block

migration tests

* delete records & repos

* delete blobs

* hook it up in route

* pettier ignore email templates

* testing & bugfixes

* testing blobs & bugfixes

* pr feedback

* make deletion test more robust

* change out handle for did on account deletion

* small cleanup

---------

Co-authored-by: Paul Frazee <[email protected]>
  • Loading branch information
dholms and pfrazee authored Feb 2, 2023
1 parent eb04cd3 commit 773f9e3
Show file tree
Hide file tree
Showing 55 changed files with 1,594 additions and 182 deletions.
15 changes: 14 additions & 1 deletion lexicons/com/atproto/account/delete.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,20 @@
"defs": {
"main": {
"type": "procedure",
"description": "Delete an account."
"description": "Delete a user account with a token and password.",
"input": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["did", "password", "token"],
"properties": {
"did": { "type": "string" },
"password": { "type": "string" },
"token": { "type": "string" }
}
}
},
"errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }]
}
}
}
10 changes: 10 additions & 0 deletions lexicons/com/atproto/account/requestDelete.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"lexicon": 1,
"id": "com.atproto.account.requestDelete",
"defs": {
"main": {
"type": "procedure",
"description": "Initiate a user account deletion via email."
}
}
}
13 changes: 13 additions & 0 deletions packages/api/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as ComAtprotoAccountCreate from './types/com/atproto/account/create'
import * as ComAtprotoAccountCreateInviteCode from './types/com/atproto/account/createInviteCode'
import * as ComAtprotoAccountDelete from './types/com/atproto/account/delete'
import * as ComAtprotoAccountGet from './types/com/atproto/account/get'
import * as ComAtprotoAccountRequestDelete from './types/com/atproto/account/requestDelete'
import * as ComAtprotoAccountRequestPasswordReset from './types/com/atproto/account/requestPasswordReset'
import * as ComAtprotoAccountResetPassword from './types/com/atproto/account/resetPassword'
import * as ComAtprotoAdminGetModerationAction from './types/com/atproto/admin/getModerationAction'
Expand Down Expand Up @@ -91,6 +92,7 @@ export * as ComAtprotoAccountCreate from './types/com/atproto/account/create'
export * as ComAtprotoAccountCreateInviteCode from './types/com/atproto/account/createInviteCode'
export * as ComAtprotoAccountDelete from './types/com/atproto/account/delete'
export * as ComAtprotoAccountGet from './types/com/atproto/account/get'
export * as ComAtprotoAccountRequestDelete from './types/com/atproto/account/requestDelete'
export * as ComAtprotoAccountRequestPasswordReset from './types/com/atproto/account/requestPasswordReset'
export * as ComAtprotoAccountResetPassword from './types/com/atproto/account/resetPassword'
export * as ComAtprotoAdminGetModerationAction from './types/com/atproto/admin/getModerationAction'
Expand Down Expand Up @@ -305,6 +307,17 @@ export class AccountNS {
})
}

requestDelete(
data?: ComAtprotoAccountRequestDelete.InputSchema,
opts?: ComAtprotoAccountRequestDelete.CallOptions,
): Promise<ComAtprotoAccountRequestDelete.Response> {
return this._service.xrpc
.call('com.atproto.account.requestDelete', opts?.qp, data, opts)
.catch((e) => {
throw ComAtprotoAccountRequestDelete.toKnownErr(e)
})
}

requestPasswordReset(
data?: ComAtprotoAccountRequestPasswordReset.InputSchema,
opts?: ComAtprotoAccountRequestPasswordReset.CallOptions,
Expand Down
39 changes: 38 additions & 1 deletion packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,33 @@ export const schemaDict = {
defs: {
main: {
type: 'procedure',
description: 'Delete an account.',
description: 'Delete a user account with a token and password.',
input: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['did', 'password', 'token'],
properties: {
did: {
type: 'string',
},
password: {
type: 'string',
},
token: {
type: 'string',
},
},
},
},
errors: [
{
name: 'ExpiredToken',
},
{
name: 'InvalidToken',
},
],
},
},
},
Expand All @@ -127,6 +153,16 @@ export const schemaDict = {
},
},
},
ComAtprotoAccountRequestDelete: {
lexicon: 1,
id: 'com.atproto.account.requestDelete',
defs: {
main: {
type: 'procedure',
description: 'Initiate a user account deletion via email.',
},
},
},
ComAtprotoAccountRequestPasswordReset: {
lexicon: 1,
id: 'com.atproto.account.requestPasswordReset',
Expand Down Expand Up @@ -3796,6 +3832,7 @@ export const ids = {
ComAtprotoAccountCreateInviteCode: 'com.atproto.account.createInviteCode',
ComAtprotoAccountDelete: 'com.atproto.account.delete',
ComAtprotoAccountGet: 'com.atproto.account.get',
ComAtprotoAccountRequestDelete: 'com.atproto.account.requestDelete',
ComAtprotoAccountRequestPasswordReset:
'com.atproto.account.requestPasswordReset',
ComAtprotoAccountResetPassword: 'com.atproto.account.resetPassword',
Expand Down
22 changes: 21 additions & 1 deletion packages/api/src/client/types/com/atproto/account/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,40 @@ import { lexicons } from '../../../../lexicons'

export interface QueryParams {}

export type InputSchema = undefined
export interface InputSchema {
did: string
password: string
token: string
[k: string]: unknown
}

export interface CallOptions {
headers?: Headers
qp?: QueryParams
encoding: 'application/json'
}

export interface Response {
success: boolean
headers: Headers
}

export class ExpiredTokenError extends XRPCError {
constructor(src: XRPCError) {
super(src.status, src.error, src.message)
}
}

export class InvalidTokenError extends XRPCError {
constructor(src: XRPCError) {
super(src.status, src.error, src.message)
}
}

export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
if (e.error === 'ExpiredToken') return new ExpiredTokenError(e)
if (e.error === 'InvalidToken') return new InvalidTokenError(e)
}
return e
}
27 changes: 27 additions & 0 deletions packages/api/src/client/types/com/atproto/account/requestDelete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers, XRPCError } from '@atproto/xrpc'
import { ValidationResult } from '@atproto/lexicon'
import { isObj, hasProp } from '../../../../util'
import { lexicons } from '../../../../lexicons'

export interface QueryParams {}

export type InputSchema = undefined

export interface CallOptions {
headers?: Headers
qp?: QueryParams
}

export interface Response {
success: boolean
headers: Headers
}

export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}
7 changes: 7 additions & 0 deletions packages/aws/src/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ export class S3BlobStore implements BlobStore {
const res = await this.getObject(cid)
return res as stream.Readable
}

async delete(cid: CID): Promise<void> {
await this.client.deleteObject({
Bucket: this.bucket,
Key: this.getStoredPath(cid),
})
}
}

export default S3BlobStore
3 changes: 2 additions & 1 deletion packages/pds/.prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
src/lexicon/**/*
src/lexicon/**/*
src/mailer/templates/**/*
6 changes: 5 additions & 1 deletion packages/pds/src/api/app/bsky/notification/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ export default function (server: Server, ctx: AppContext) {

let notifBuilder = ctx.db.db
.selectFrom('user_notification as notif')
.innerJoin('ipld_block', 'ipld_block.cid', 'notif.recordCid')
.innerJoin('ipld_block', (join) =>
join
.onRef('ipld_block.cid', '=', 'notif.recordCid')
.onRef('ipld_block.creator', '=', 'notif.author'),
)
.innerJoin('did_handle as author', 'author.did', 'notif.author')
.leftJoin(
'profile as author_profile',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,14 @@ import { InvalidRequestError } from '@atproto/xrpc-server'
import * as crypto from '@atproto/crypto'
import * as handleLib from '@atproto/handle'
import { cidForCbor } from '@atproto/common'
import { Server, APP_BSKY_SYSTEM } from '../../../lexicon'
import { countAll } from '../../../db/util'
import * as lex from '../../../lexicon/lexicons'
import * as repo from '../../../repo'
import { UserAlreadyExistsError } from '../../../services/actor'
import AppContext from '../../../context'
import { Server, APP_BSKY_SYSTEM } from '../../../../lexicon'
import { countAll } from '../../../../db/util'
import * as lex from '../../../../lexicon/lexicons'
import * as repo from '../../../../repo'
import { UserAlreadyExistsError } from '../../../../services/actor'
import AppContext from '../../../../context'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.server.getAccountsConfig(() => {
const availableUserDomains = ctx.cfg.availableUserDomains
const inviteCodeRequired = ctx.cfg.inviteRequired
const privacyPolicy = ctx.cfg.privacyPolicyUrl
const termsOfService = ctx.cfg.termsOfServiceUrl

return {
encoding: 'application/json',
body: {
availableUserDomains,
inviteCodeRequired,
links: { privacyPolicy, termsOfService },
},
}
})

server.com.atproto.account.get(() => {
throw new InvalidRequestError('Not implemented')
})

server.com.atproto.account.create(async ({ input, req }) => {
const { email, password, inviteCode, recoveryKey } = input.body

Expand Down Expand Up @@ -175,9 +155,4 @@ export default function (server: Server, ctx: AppContext) {
},
}
})

server.com.atproto.account.delete(() => {
// TODO
throw new InvalidRequestError('Not implemented')
})
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as crypto from '@atproto/crypto'
import * as uint8arrays from 'uint8arrays'
import { Server } from '../../../lexicon'
import AppContext from '../../../context'
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.account.createInviteCode({
Expand Down
78 changes: 78 additions & 0 deletions packages/pds/src/api/com/atproto/account/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { AuthRequiredError } from '@atproto/xrpc-server'
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
import Database from '../../../../db'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.account.delete(async ({ input }) => {
const { did, password, token } = input.body
const validPass = await ctx.services
.actor(ctx.db)
.verifyUserDidPassword(did, password)
if (!validPass) {
throw new AuthRequiredError('Invalid did or password')
}

const tokenInfo = await ctx.db.db
.selectFrom('did_handle')
.innerJoin('delete_account_token as token', 'token.did', 'did_handle.did')
.where('did_handle.did', '=', did)
.where('token.token', '=', token)
.select([
'token.token as token',
'token.requestedAt as requestedAt',
'token.did as did',
])
.executeTakeFirst()

if (!tokenInfo) {
return createInvalidTokenError()
}

const now = new Date()
const requestedAt = new Date(tokenInfo.requestedAt)
const expiresAt = new Date(requestedAt.getTime() + 15 * minsToMs)
if (now > expiresAt) {
await removeDeleteToken(ctx.db, tokenInfo.did)
return createExpiredTokenError()
}

await ctx.db.transaction(async (dbTxn) => {
await removeDeleteToken(dbTxn, did)
await ctx.services.record(dbTxn).deleteForUser(did)
await ctx.services.repo(dbTxn).deleteRepo(did)
await ctx.services.actor(dbTxn).deleteUser(did)
})
})
}

type ErrorResponse = {
status: number
error: string
message: string
}

const minsToMs = 60 * 1000

const createInvalidTokenError = (): ErrorResponse & {
error: 'InvalidToken'
} => ({
status: 400,
error: 'InvalidToken',
message: 'Token is invalid',
})

const createExpiredTokenError = (): ErrorResponse & {
error: 'ExpiredToken'
} => ({
status: 400,
error: 'ExpiredToken',
message: 'The password reset token has expired',
})

const removeDeleteToken = async (db: Database, did: string) => {
await db.db
.deleteFrom('delete_account_token')
.where('delete_account_token.did', '=', did)
.execute()
}
9 changes: 9 additions & 0 deletions packages/pds/src/api/com/atproto/account/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { InvalidRequestError } from '@atproto/xrpc-server'
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'

export default function (server: Server, _ctx: AppContext) {
server.com.atproto.account.get(() => {
throw new InvalidRequestError('Not implemented')
})
}
Loading

0 comments on commit 773f9e3

Please sign in to comment.