···77 * that might have been taken down by admins
88 */
99export const takedownFilterMiddleware = async (c: Context, next: Next) => {
1010- // Skip filtering for admin routes and non-content routes
1111- const path = c.req.path
1212- if (path.startsWith('/admin/') || path === '/' || path.includes('favicon') || path.includes('xrpc/com.atproto.admin.updateSubjectStatus')) {
1010+ // Skip filtering if user is an admin
1111+ const isAdmin = c.get('isAdmin') as boolean | undefined
1212+ if (isAdmin) {
1313 await next()
1414 return
1515 }
+51-5
services/appview/src/routes/admin/takedowns.ts
···33import { z } from 'zod'
44import { HTTPException } from 'hono/http-exception'
55import { TakedownService } from '../../services/takedown.js'
66-import { adminAuthMiddleware } from '../../auth/middleware.js'
66+import { authMiddleware } from '../../auth/middleware.js'
77+import { Database } from '../../db.js'
88+import { AtUri } from '@atproto/syntax'
79810type TakedownContext = {
911 takedownService: TakedownService
1212+ db: Database
1013}
11141215export const createTakedownRouter = (ctx: TakedownContext) => {
···1417 const takedownService = ctx.takedownService
15181619 // Apply admin auth middleware to all admin routes
1717- takedownRoutes.use('/admin/*', adminAuthMiddleware)
1818-1919- // No auth needed for the Ozone integration endpoint as it will use its own auth
2020- // This will be protected by adminAuthMiddleware before processing
2020+ takedownRoutes.use('/admin/*', (c, next) => authMiddleware(c, next, true))
21212222 takedownRoutes.post('/admin/takedowns', zValidator('json', z.object({
2323 targetUri: z.string(),
···220220 return c.json({ isTakenDown })
221221 } catch (error) {
222222 throw new HTTPException(500, { message: 'Failed to check blob takedown status' })
223223+ }
224224+ })
225225+226226+ // Get a specific taken down record with its content
227227+ takedownRoutes.get('/admin/takedowns/content/:uri', async (c) => {
228228+ const uri = c.req.param('uri')
229229+230230+ try {
231231+ // First check if this content is taken down
232232+ const takedown = await takedownService.getTakedown(uri)
233233+ if (!takedown) {
234234+ return c.json({ error: 'Content is not taken down' }, 404)
235235+ }
236236+237237+ // Parse the URI to extract components
238238+ const atUri = new AtUri(uri)
239239+ const collection = atUri.collection
240240+ const did = atUri.hostname
241241+ let record = null
242242+243243+ // Get record based on collection
244244+ if (collection.includes('post')) {
245245+ record = await ctx.db.models.Post.findOne({ uri }).lean()
246246+ } else if (collection.includes('repost')) {
247247+ record = await ctx.db.models.Repost.findOne({ uri }).lean()
248248+ } else if (collection.includes('like')) {
249249+ record = await ctx.db.models.Like.findOne({ uri }).lean()
250250+ } else if (collection.includes('follow')) {
251251+ record = await ctx.db.models.Follow.findOne({ uri }).lean()
252252+ } else if (collection.includes('block')) {
253253+ record = await ctx.db.models.Block.findOne({ uri }).lean()
254254+ } else if (collection.includes('profile')) {
255255+ // For profiles we need to extract the DID
256256+ record = await ctx.db.models.Profile.findOne({ authorDid: did }).lean()
257257+ }
258258+259259+ if (!record) {
260260+ return c.json({ error: 'Record content not found in database' }, 404)
261261+ }
262262+263263+ return c.json({
264264+ takedown,
265265+ record
266266+ })
267267+ } catch (error) {
268268+ throw new HTTPException(500, { message: 'Failed to fetch taken down content' })
223269 }
224270 })
225271
···33import { z } from 'zod'
44import { HTTPException } from 'hono/http-exception'
55import { TakedownService } from '../../../../services/takedown.js'
66-import { adminAuthMiddleware } from '../../../../auth/middleware.js'
66+import { authMiddleware } from '../../../../auth/middleware.js'
77import type * as ComAtprotoAdminUpdateSubjectStatus from '../../../../lexicon/types/com/atproto/admin/updateSubjectStatus.js'
88import type * as ComAtprotoAdminDefs from '../../../../lexicon/types/com/atproto/admin/defs.js'
99import type * as ComAtprotoRepoStrongRef from '../../../../lexicon/types/com/atproto/repo/strongRef.js'
···2121 // XRPC endpoint for Ozone integration: com.atproto.admin.updateSubjectStatus
2222 router.post(
2323 '/xrpc/com.atproto.admin.updateSubjectStatus',
2424- adminAuthMiddleware,
2424+ (c, next) => authMiddleware(c, next, true),
2525 zValidator(
2626 'json',
2727 z.object({
···11+import { AtUri } from '@atproto/syntax'
22+import { InvalidRequestError } from '@atproto/xrpc-server'
33+import { Hono } from 'hono'
44+55+import { optionalAuthMiddleware } from '../../../../auth/middleware.js'
66+import { AppContext } from '../../../../index.js'
77+import { OutputSchema } from '../../../../lexicon/types/com/atproto/repo/getRecord.js'
88+99+export const createGetRecordRouter = (ctx: AppContext) => {
1010+ const router = new Hono()
1111+1212+ router.get(
1313+ '/xrpc/com.atproto.repo.getRecord',
1414+ optionalAuthMiddleware,
1515+ async (c) => {
1616+ const { repo, collection, rkey, cid } = c.req.query()
1717+ const viewerDid = c.get('did') as string | undefined
1818+ const isAdmin = c.get('isAdmin') as boolean | undefined
1919+2020+ if (!repo || !collection || !rkey) {
2121+ return c.json({ error: 'Missing required parameters' }, 400)
2222+ }
2323+2424+ // Resolve the handle to DID if needed
2525+ let did
2626+ try {
2727+ if (repo.startsWith('did:')) {
2828+ did = repo
2929+ } else {
3030+ // Assume it's a handle
3131+ const didDoc = await ctx.resolver.resolveHandleToDidDoc(repo)
3232+ did = didDoc.did
3333+ }
3434+ } catch (err) {
3535+ throw new InvalidRequestError(`Could not find repo: ${repo}`)
3636+ }
3737+3838+ if (!did) {
3939+ throw new InvalidRequestError(`Could not find repo: ${repo}`)
4040+ }
4141+4242+ // Create the URI
4343+ const uri = AtUri.make(did, collection, rkey).toString()
4444+4545+ // Get the record based on the collection type
4646+ try {
4747+ let record = null
4848+4949+ // Check which collection to query based on the NSID
5050+ if (collection.includes('post') || collection.endsWith('post')) {
5151+ record = await ctx.db.models.Post.findOne({ uri }).lean()
5252+ } else if (collection.includes('repost')) {
5353+ record = await ctx.db.models.Repost.findOne({ uri }).lean()
5454+ } else if (collection.includes('like')) {
5555+ record = await ctx.db.models.Like.findOne({ uri }).lean()
5656+ } else if (collection.includes('look')) {
5757+ record = await ctx.db.models.Look.findOne({ uri }).lean()
5858+ } else if (collection.includes('profile')) {
5959+ record = await ctx.db.models.Profile.findOne({ authorDid: did }).lean()
6060+ } else if (collection.includes('follow')) {
6161+ record = await ctx.db.models.Follow.findOne({ uri }).lean()
6262+ } else if (collection.includes('block')) {
6363+ record = await ctx.db.models.Block.findOne({ uri }).lean()
6464+ }
6565+6666+ if (!record || (cid && record.cid !== cid)) {
6767+ // For admins, provide more detailed information about what we tried to query
6868+ if (isAdmin) {
6969+ ctx.logger.info({
7070+ uri,
7171+ collection,
7272+ did,
7373+ rkey,
7474+ cid,
7575+ foundRecord: !!record,
7676+ cidMatch: record ? (cid ? record.cid === cid : true) : false,
7777+ }, 'Admin record lookup failed')
7878+ }
7979+ throw new InvalidRequestError(`Could not locate record: ${uri}`)
8080+ }
8181+8282+ // Check if the record is subject to a takedown
8383+ const takedown = await ctx.takedownService.getTakedown(uri)
8484+8585+ // If record is taken down and user is not an admin, deny access
8686+ if (takedown && !isAdmin) {
8787+ throw new InvalidRequestError(`Record is taken down: ${uri}`)
8888+ }
8989+9090+ // Format the response according to the output schema
9191+ const response: OutputSchema & { takedown?: any } = {
9292+ uri: uri,
9393+ cid: record.cid,
9494+ value: record
9595+ }
9696+9797+ // Include takedown info for admins
9898+ if (isAdmin && takedown) {
9999+ response.takedown = {
100100+ reason: takedown.reason,
101101+ takenDownBy: takedown.takenDownBy,
102102+ takenDownAt: takedown.takenDownAt,
103103+ warning: 'This content has been taken down and is only visible to admins'
104104+ }
105105+ }
106106+107107+ return c.json(response)
108108+ } catch (err) {
109109+ if (err instanceof InvalidRequestError) {
110110+ throw err
111111+ }
112112+ throw new InvalidRequestError(`Error retrieving record: ${uri}`)
113113+ }
114114+ },
115115+ )
116116+117117+ return router
118118+}
119119+120120+export default (ctx: AppContext) => createGetRecordRouter(ctx)
+16
services/appview/src/services/takedown.ts
···6666 return !!takedown
6767 }
68686969+ /**
7070+ * Get takedown information for a URI if it exists
7171+ * @param uri The URI of the content to check
7272+ * @returns Takedown information or null if not taken down
7373+ */
7474+ async getTakedown(uri: string): Promise<{
7575+ targetUri: string
7676+ targetCid: string
7777+ reason: string
7878+ takenDownBy: string
7979+ takenDownAt: string
8080+ } | null> {
8181+ const takedown = await this.db.models.Takedown.findOne({ targetUri: uri }).lean()
8282+ return takedown
8383+ }
8484+6985 // Add a method to check if a repo is taken down
7086 async isRepoTakenDown(did: string): Promise<boolean> {
7187 const takedown = await this.db.models.RepoTakedown.findOne({ did })