mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-02-08 21:30:08 +00:00
Age assurance override env var
This commit is contained in:
@@ -139,6 +139,15 @@ AWS_SECRET_ACCESS_KEY=minioadmin
|
||||
# REPORT_SERVICE_URL=https://mod.bsky.app
|
||||
# REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac
|
||||
# =============================================================================
|
||||
# Age Assurance Override
|
||||
# =============================================================================
|
||||
# Enable this if you have separately assured the ages of your users
|
||||
# (e.g., through your own age verification process). When enabled, the PDS
|
||||
# will return "assured" status for age assurance checks instead of proxying
|
||||
# to the appview. This helps migrated users avoid the age assurance
|
||||
# catch-22 on bsky.app.
|
||||
# PDS_AGE_ASSURANCE_OVERRIDE=1
|
||||
# =============================================================================
|
||||
# Miscellaneous
|
||||
# =============================================================================
|
||||
# Allow HTTP for proxy requests (development only)
|
||||
|
||||
16
.sqlx/query-6efda9a01aff3277386c617e8500150271613b6779178816d9acfb244b48066c.json
generated
Normal file
16
.sqlx/query-6efda9a01aff3277386c617e8500150271613b6779178816d9acfb244b48066c.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)\n ON CONFLICT (user_id, name) DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6efda9a01aff3277386c617e8500150271613b6779178816d9acfb244b48066c"
|
||||
}
|
||||
16
.sqlx/query-839b7593dd13cfc4cd303a626c7e17c93d702ff1a8be8018f3f21e8fd3d550a8.json
generated
Normal file
16
.sqlx/query-839b7593dd13cfc4cd303a626c7e17c93d702ff1a8be8018f3f21e8fd3d550a8.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)\n ON CONFLICT (user_id, name) DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "839b7593dd13cfc4cd303a626c7e17c93d702ff1a8be8018f3f21e8fd3d550a8"
|
||||
}
|
||||
22
.sqlx/query-b2294557cfcc57a9fa2ed90602ea66ce90ae92d28b41886c5bb9b81e4b53eaa2.json
generated
Normal file
22
.sqlx/query-b2294557cfcc57a9fa2ed90602ea66ce90ae92d28b41886c5bb9b81e4b53eaa2.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT created_at FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b2294557cfcc57a9fa2ed90602ea66ce90ae92d28b41886c5bb9b81e4b53eaa2"
|
||||
}
|
||||
@@ -131,10 +131,12 @@ export class AtprotoClient {
|
||||
error: "Unknown",
|
||||
message: res.statusText,
|
||||
}));
|
||||
const error = new Error(err.message || err.error || res.statusText) as Error & {
|
||||
status: number;
|
||||
error: string;
|
||||
};
|
||||
const error = new Error(err.message || err.error || res.statusText) as
|
||||
& Error
|
||||
& {
|
||||
status: number;
|
||||
error: string;
|
||||
};
|
||||
error.status = res.status;
|
||||
error.error = err.error;
|
||||
throw error;
|
||||
@@ -272,10 +274,12 @@ export class AtprotoClient {
|
||||
error: "Unknown",
|
||||
message: res.statusText,
|
||||
}));
|
||||
const error = new Error(err.message || err.error || res.statusText) as Error & {
|
||||
status: number;
|
||||
error: string;
|
||||
};
|
||||
const error = new Error(err.message || err.error || res.statusText) as
|
||||
& Error
|
||||
& {
|
||||
status: number;
|
||||
error: string;
|
||||
};
|
||||
error.status = res.status;
|
||||
error.error = err.error;
|
||||
throw error;
|
||||
@@ -369,9 +373,13 @@ export class AtprotoClient {
|
||||
}
|
||||
|
||||
async deactivateAccount(migratingTo?: string): Promise<void> {
|
||||
apiLog("POST", `${this.baseUrl}/xrpc/com.atproto.server.deactivateAccount`, {
|
||||
migratingTo,
|
||||
});
|
||||
apiLog(
|
||||
"POST",
|
||||
`${this.baseUrl}/xrpc/com.atproto.server.deactivateAccount`,
|
||||
{
|
||||
migratingTo,
|
||||
},
|
||||
);
|
||||
const start = Date.now();
|
||||
try {
|
||||
const body: { migratingTo?: string } = {};
|
||||
@@ -503,10 +511,12 @@ export class AtprotoClient {
|
||||
error: "Unknown",
|
||||
message: res.statusText,
|
||||
}));
|
||||
const error = new Error(err.message || err.error || res.statusText) as Error & {
|
||||
status: number;
|
||||
error: string;
|
||||
};
|
||||
const error = new Error(err.message || err.error || res.statusText) as
|
||||
& Error
|
||||
& {
|
||||
status: number;
|
||||
error: string;
|
||||
};
|
||||
error.status = res.status;
|
||||
error.error = err.error;
|
||||
throw error;
|
||||
@@ -549,7 +559,8 @@ export async function getOAuthServerMetadata(
|
||||
return directRes.json();
|
||||
}
|
||||
|
||||
const protectedResourceUrl = `${pdsUrl}/.well-known/oauth-protected-resource`;
|
||||
const protectedResourceUrl =
|
||||
`${pdsUrl}/.well-known/oauth-protected-resource`;
|
||||
const protectedRes = await fetch(protectedResourceUrl);
|
||||
if (!protectedRes.ok) {
|
||||
return null;
|
||||
@@ -561,7 +572,9 @@ export async function getOAuthServerMetadata(
|
||||
return null;
|
||||
}
|
||||
|
||||
const authServerUrl = `${authServers[0]}/.well-known/oauth-authorization-server`;
|
||||
const authServerUrl = `${
|
||||
authServers[0]
|
||||
}/.well-known/oauth-authorization-server`;
|
||||
const authServerRes = await fetch(authServerUrl);
|
||||
if (!authServerRes.ok) {
|
||||
return null;
|
||||
@@ -595,7 +608,10 @@ export function base64UrlEncode(buffer: Uint8Array | ArrayBuffer): string {
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(
|
||||
/=+$/,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
export function base64UrlDecode(base64url: string): Uint8Array {
|
||||
@@ -730,14 +746,17 @@ export async function exchangeOAuthCode(
|
||||
error_description: res.statusText,
|
||||
}));
|
||||
throw new Error(
|
||||
retryErr.error_description || retryErr.error || "Token exchange failed",
|
||||
retryErr.error_description || retryErr.error ||
|
||||
"Token exchange failed",
|
||||
);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(err.error_description || err.error || "Token exchange failed");
|
||||
throw new Error(
|
||||
err.error_description || err.error || "Token exchange failed",
|
||||
);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
InboundMigrationState,
|
||||
InboundStep,
|
||||
MigrationProgress,
|
||||
OAuthServerMetadata,
|
||||
OutboundMigrationState,
|
||||
OutboundStep,
|
||||
PasskeyAccountSetup,
|
||||
@@ -86,7 +85,6 @@ export function createInboundMigrationFlow() {
|
||||
let sourceClient: AtprotoClient | null = null;
|
||||
let localClient: AtprotoClient | null = null;
|
||||
let localServerInfo: ServerDescription | null = null;
|
||||
let sourceOAuthMetadata: OAuthServerMetadata | null = null;
|
||||
|
||||
function setStep(step: InboundStep) {
|
||||
state.step = step;
|
||||
@@ -271,10 +269,14 @@ export function createInboundMigrationFlow() {
|
||||
if (state.authMethod === "passkey" && state.passkeySetupToken) {
|
||||
localClient = createLocalClient();
|
||||
setStep("passkey-setup");
|
||||
migrationLog("handleOAuthCallback: Resuming passkey flow at passkey-setup");
|
||||
migrationLog(
|
||||
"handleOAuthCallback: Resuming passkey flow at passkey-setup",
|
||||
);
|
||||
} else {
|
||||
setStep("email-verify");
|
||||
migrationLog("handleOAuthCallback: Resuming at email-verify for re-auth");
|
||||
migrationLog(
|
||||
"handleOAuthCallback: Resuming at email-verify for re-auth",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setStep(targetStep);
|
||||
@@ -337,7 +339,9 @@ export function createInboundMigrationFlow() {
|
||||
serverDid: serverInfo.did,
|
||||
});
|
||||
|
||||
migrationLog("startMigration: Getting service auth token from source PDS");
|
||||
migrationLog(
|
||||
"startMigration: Getting service auth token from source PDS",
|
||||
);
|
||||
const { token } = await sourceClient.getServiceAuth(
|
||||
serverInfo.did,
|
||||
"com.atproto.server.createAccount",
|
||||
@@ -361,7 +365,10 @@ export function createInboundMigrationFlow() {
|
||||
inviteCode: passkeyParams.inviteCode,
|
||||
stateInviteCode: state.inviteCode,
|
||||
});
|
||||
passkeySetup = await localClient.createPasskeyAccount(passkeyParams, token);
|
||||
passkeySetup = await localClient.createPasskeyAccount(
|
||||
passkeyParams,
|
||||
token,
|
||||
);
|
||||
migrationLog("startMigration: Passkey account created on NEW PDS", {
|
||||
did: passkeySetup.did,
|
||||
hasAccessJwt: !!passkeySetup.accessJwt,
|
||||
@@ -743,7 +750,9 @@ export function createInboundMigrationFlow() {
|
||||
migrationLog("Activating account on NEW PDS");
|
||||
const activateStart = Date.now();
|
||||
await localClient.activateAccount();
|
||||
migrationLog("Account activated", { durationMs: Date.now() - activateStart });
|
||||
migrationLog("Account activated", {
|
||||
durationMs: Date.now() - activateStart,
|
||||
});
|
||||
setProgress({ activated: true });
|
||||
|
||||
setProgress({ currentOperation: "Deactivating old account..." });
|
||||
@@ -757,7 +766,9 @@ export function createInboundMigrationFlow() {
|
||||
setProgress({ deactivated: true });
|
||||
} catch (deactivateErr) {
|
||||
const err = deactivateErr as Error & { error?: string };
|
||||
migrationLog("Could not deactivate on source PDS", { error: err.message });
|
||||
migrationLog("Could not deactivate on source PDS", {
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
|
||||
migrationLog("completeDidWebMigration SUCCESS");
|
||||
|
||||
@@ -352,7 +352,9 @@ label.auth-option {
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
margin-bottom: 0;
|
||||
transition: border-color var(--transition-normal), background-color var(--transition-normal);
|
||||
transition:
|
||||
border-color var(--transition-normal),
|
||||
background-color var(--transition-normal);
|
||||
}
|
||||
|
||||
.auth-option:hover {
|
||||
|
||||
@@ -77,7 +77,8 @@ describe("Dashboard", () => {
|
||||
setupAuthenticatedUser({ isAdmin: true });
|
||||
mockEndpoint(
|
||||
"com.atproto.server.describeServer",
|
||||
() => jsonResponse(mockData.describeServer({ inviteCodeRequired: true })),
|
||||
() =>
|
||||
jsonResponse(mockData.describeServer({ inviteCodeRequired: true })),
|
||||
);
|
||||
render(Dashboard);
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/svelte";
|
||||
import Login from "../routes/Login.svelte";
|
||||
import {
|
||||
@@ -15,8 +15,9 @@ describe("Login", () => {
|
||||
clearMocks();
|
||||
setupFetchMock();
|
||||
globalThis.location.hash = "";
|
||||
mockEndpoint("/oauth/par", () =>
|
||||
jsonResponse({ request_uri: "urn:mock:request" })
|
||||
mockEndpoint(
|
||||
"/oauth/par",
|
||||
() => jsonResponse({ request_uri: "urn:mock:request" }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -85,8 +86,11 @@ describe("Login", () => {
|
||||
error: null,
|
||||
savedAccounts,
|
||||
});
|
||||
mockEndpoint("com.atproto.server.getSession", () =>
|
||||
jsonResponse(mockData.session({ handle: "alice.test.tranquil.dev" })));
|
||||
mockEndpoint(
|
||||
"com.atproto.server.getSession",
|
||||
() =>
|
||||
jsonResponse(mockData.session({ handle: "alice.test.tranquil.dev" })),
|
||||
);
|
||||
});
|
||||
|
||||
it("displays saved accounts list", async () => {
|
||||
|
||||
@@ -110,8 +110,10 @@ describe("Settings", () => {
|
||||
capturedBody = JSON.parse((options?.body as string) || "{}");
|
||||
return jsonResponse({});
|
||||
});
|
||||
mockEndpoint("com.atproto.server.getSession", () =>
|
||||
jsonResponse(mockData.session()));
|
||||
mockEndpoint(
|
||||
"com.atproto.server.getSession",
|
||||
() => jsonResponse(mockData.session()),
|
||||
);
|
||||
render(Settings);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: /change email/i }))
|
||||
@@ -144,8 +146,10 @@ describe("Settings", () => {
|
||||
() => jsonResponse({ tokenRequired: true }),
|
||||
);
|
||||
mockEndpoint("com.atproto.server.updateEmail", () => jsonResponse({}));
|
||||
mockEndpoint("com.atproto.server.getSession", () =>
|
||||
jsonResponse(mockData.session()));
|
||||
mockEndpoint(
|
||||
"com.atproto.server.getSession",
|
||||
() => jsonResponse(mockData.session()),
|
||||
);
|
||||
render(Settings);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: /change email/i }))
|
||||
@@ -188,7 +192,9 @@ describe("Settings", () => {
|
||||
expect(screen.getByRole("button", { name: /cancel/i }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
const emailSection = screen.getByRole("heading", { name: /change email/i })
|
||||
const emailSection = screen.getByRole("heading", {
|
||||
name: /change email/i,
|
||||
})
|
||||
.closest("section");
|
||||
const cancelButton = emailSection?.querySelector("button.secondary");
|
||||
if (cancelButton) {
|
||||
@@ -220,8 +226,10 @@ describe("Settings", () => {
|
||||
describe("handle change", () => {
|
||||
beforeEach(() => {
|
||||
setupAuthenticatedUser();
|
||||
mockEndpoint("com.atproto.server.describeServer", () =>
|
||||
jsonResponse(mockData.describeServer()));
|
||||
mockEndpoint(
|
||||
"com.atproto.server.describeServer",
|
||||
() => jsonResponse(mockData.describeServer()),
|
||||
);
|
||||
});
|
||||
it("displays current handle", async () => {
|
||||
render(Settings);
|
||||
@@ -255,8 +263,10 @@ describe("Settings", () => {
|
||||
});
|
||||
it("shows success message after handle change", async () => {
|
||||
mockEndpoint("com.atproto.identity.updateHandle", () => jsonResponse({}));
|
||||
mockEndpoint("com.atproto.server.getSession", () =>
|
||||
jsonResponse(mockData.session()));
|
||||
mockEndpoint(
|
||||
"com.atproto.server.getSession",
|
||||
() => jsonResponse(mockData.session()),
|
||||
);
|
||||
render(Settings);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/new handle/i)).toBeInTheDocument();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
base64UrlDecode,
|
||||
base64UrlEncode,
|
||||
@@ -351,7 +351,13 @@ describe("migration/atproto-client", () => {
|
||||
|
||||
it("returns null and clears storage for expired key (> 24 hours)", async () => {
|
||||
const stored = {
|
||||
privateJwk: { kty: "EC", crv: "P-256", x: "test", y: "test", d: "test" },
|
||||
privateJwk: {
|
||||
kty: "EC",
|
||||
crv: "P-256",
|
||||
x: "test",
|
||||
y: "test",
|
||||
d: "test",
|
||||
},
|
||||
publicJwk: { kty: "EC", crv: "P-256", x: "test", y: "test" },
|
||||
thumbprint: "test-thumb",
|
||||
createdAt: Date.now() - 25 * 60 * 60 * 1000,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
clearMigrationState,
|
||||
getResumeInfo,
|
||||
|
||||
@@ -63,7 +63,9 @@ describe("migration/types", () => {
|
||||
});
|
||||
|
||||
it("can check if error is MigrationError", () => {
|
||||
const error = new MigrationError("Test", "ERR_TEST", true, { foo: "bar" });
|
||||
const error = new MigrationError("Test", "ERR_TEST", true, {
|
||||
foo: "bar",
|
||||
});
|
||||
|
||||
if (error instanceof MigrationError) {
|
||||
expect(error.code).toBe("ERR_TEST");
|
||||
|
||||
119
src/api/age_assurance.rs
Normal file
119
src/api/age_assurance.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use crate::auth::{extract_bearer_token_from_header, validate_bearer_token};
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
body::Bytes,
|
||||
extract::{Path, RawQuery, State},
|
||||
http::{HeaderMap, Method, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub async fn get_state(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
RawQuery(query): RawQuery,
|
||||
) -> Response {
|
||||
if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_err() {
|
||||
return proxy_to_appview(state, headers, "app.bsky.ageassurance.getState", query).await;
|
||||
}
|
||||
|
||||
let created_at = get_account_created_at(&state, &headers).await;
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"state": {
|
||||
"status": "assured",
|
||||
"access": "full",
|
||||
"lastInitiatedAt": now
|
||||
},
|
||||
"metadata": {
|
||||
"accountCreatedAt": created_at
|
||||
}
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub async fn get_age_assurance_state(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
RawQuery(query): RawQuery,
|
||||
) -> Response {
|
||||
if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_err() {
|
||||
return proxy_to_appview(
|
||||
state,
|
||||
headers,
|
||||
"app.bsky.unspecced.getAgeAssuranceState",
|
||||
query,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
(StatusCode::OK, Json(json!({"status": "assured"}))).into_response()
|
||||
}
|
||||
|
||||
async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option<String> {
|
||||
let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok());
|
||||
tracing::debug!(?auth_header, "age assurance: extracting token");
|
||||
|
||||
let token = extract_bearer_token_from_header(auth_header)?;
|
||||
tracing::debug!("age assurance: got token, validating");
|
||||
|
||||
let auth_user = match validate_bearer_token(&state.db, &token).await {
|
||||
Ok(user) => {
|
||||
tracing::debug!(did = %user.did, "age assurance: validated user");
|
||||
user
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(?e, "age assurance: token validation failed");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let row = match sqlx::query!("SELECT created_at FROM users WHERE did = $1", auth_user.did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(r) => {
|
||||
tracing::debug!(?r, "age assurance: query result");
|
||||
r
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(?e, "age assurance: query failed");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
row.map(|r| r.created_at.to_rfc3339())
|
||||
}
|
||||
|
||||
async fn proxy_to_appview(
|
||||
state: AppState,
|
||||
headers: HeaderMap,
|
||||
method: &str,
|
||||
query: Option<String>,
|
||||
) -> Response {
|
||||
if headers.get("atproto-proxy").is_none() {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"error": "InvalidRequest",
|
||||
"message": "Missing required atproto-proxy header"
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
crate::api::proxy::proxy_handler(
|
||||
State(state),
|
||||
Path(method.to_string()),
|
||||
Method::GET,
|
||||
headers,
|
||||
RawQuery(query),
|
||||
Bytes::new(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -986,6 +986,24 @@ pub async fn create_account(
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok() {
|
||||
let birthdate_pref = json!({
|
||||
"$type": "app.bsky.actor.defs#personalDetailsPref",
|
||||
"birthDate": "1998-05-06T00:00:00.000Z"
|
||||
});
|
||||
if let Err(e) = sqlx::query!(
|
||||
"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)
|
||||
ON CONFLICT (user_id, name) DO NOTHING",
|
||||
user_id,
|
||||
"app.bsky.actor.defs#personalDetailsPref",
|
||||
birthdate_pref
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to set default birthdate preference: {:?}", e);
|
||||
}
|
||||
}
|
||||
if let Err(e) = tx.commit().await {
|
||||
error!("Error committing transaction: {:?}", e);
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod actor;
|
||||
pub mod admin;
|
||||
pub mod age_assurance;
|
||||
pub mod delegation;
|
||||
pub mod error;
|
||||
pub mod identity;
|
||||
|
||||
@@ -478,6 +478,27 @@ pub async fn import_repo(
|
||||
{
|
||||
warn!("Failed to sequence import event: {:?}", e);
|
||||
}
|
||||
if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok() {
|
||||
let birthdate_pref = json!({
|
||||
"$type": "app.bsky.actor.defs#personalDetailsPref",
|
||||
"birthDate": "1998-05-06T00:00:00.000Z"
|
||||
});
|
||||
if let Err(e) = sqlx::query!(
|
||||
"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)
|
||||
ON CONFLICT (user_id, name) DO NOTHING",
|
||||
user_id,
|
||||
"app.bsky.actor.defs#personalDetailsPref",
|
||||
birthdate_pref
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Failed to set default birthdate preference for migrated user: {:?}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
(StatusCode::OK, Json(json!({}))).into_response()
|
||||
}
|
||||
Err(ImportError::SizeLimitExceeded) => (
|
||||
|
||||
@@ -706,6 +706,25 @@ pub async fn create_passkey_account(
|
||||
.await;
|
||||
}
|
||||
|
||||
if std::env::var("PDS_AGE_ASSURANCE_OVERRIDE").is_ok() {
|
||||
let birthdate_pref = json!({
|
||||
"$type": "app.bsky.actor.defs#personalDetailsPref",
|
||||
"birthDate": "1998-05-06T00:00:00.000Z"
|
||||
});
|
||||
if let Err(e) = sqlx::query!(
|
||||
"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)
|
||||
ON CONFLICT (user_id, name) DO NOTHING",
|
||||
user_id,
|
||||
"app.bsky.actor.defs#personalDetailsPref",
|
||||
birthdate_pref
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to set default birthdate preference: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = tx.commit().await {
|
||||
error!("Error committing transaction: {:?}", e);
|
||||
return (
|
||||
|
||||
@@ -28,6 +28,7 @@ pub struct AuditLogEntry {
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn log_delegation_action(
|
||||
pool: &PgPool,
|
||||
delegated_did: &str,
|
||||
|
||||
@@ -626,6 +626,14 @@ pub fn app(state: AppState) -> Router {
|
||||
"/xrpc/com.tranquil.delegation.createDelegatedAccount",
|
||||
post(api::delegation::create_delegated_account),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/app.bsky.ageassurance.getState",
|
||||
get(api::age_assurance::get_state),
|
||||
)
|
||||
.route(
|
||||
"/xrpc/app.bsky.unspecced.getAgeAssuranceState",
|
||||
get(api::age_assurance::get_age_assurance_state),
|
||||
)
|
||||
.route("/xrpc/{*method}", any(api::proxy::proxy_handler))
|
||||
.layer(DefaultBodyLimit::max(util::get_max_blob_size()))
|
||||
.layer(middleware::from_fn(metrics::metrics_middleware))
|
||||
|
||||
Reference in New Issue
Block a user