feat: Implement identity lock screen for sensitive user actions and introduce avatar generation.

This commit is contained in:
Christomatt
2026-01-30 04:44:57 +01:00
parent 4c9afa42fe
commit 10a54a0ea9
16 changed files with 519 additions and 134 deletions
+63
View File
@@ -0,0 +1,63 @@
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { v4 as uuidv4 } from 'uuid';
export async function generateAndUploadAvatar(handle: string): Promise<string | null> {
try {
// 1. Fetch the avatar from DiceBear
const dicebearUrl = `https://api.dicebear.com/9.x/bottts-neutral/svg?seed=${handle}`;
const response = await fetch(dicebearUrl);
if (!response.ok) {
console.error(`Failed to fetch avatar from DiceBear: ${response.statusText}`);
return null;
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
// 2. Upload to S3
const s3 = new S3Client({
region: process.env.STORAGE_REGION || 'us-east-1',
endpoint: process.env.STORAGE_ENDPOINT,
credentials: {
accessKeyId: process.env.STORAGE_ACCESS_KEY || '',
secretAccessKey: process.env.STORAGE_SECRET_KEY || '',
},
forcePathStyle: true,
});
const bucket = process.env.STORAGE_BUCKET || 'synapsis';
// Sanitize handle for filename just in case
const safeHandle = handle.replace(/[^a-zA-Z0-9]/g, '');
const filename = `${uuidv4()}-${safeHandle}-avatar.svg`;
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: filename,
Body: buffer,
ContentType: 'image/svg+xml',
ACL: 'public-read',
}));
// 3. Construct Public URL
let url = '';
if (process.env.STORAGE_PUBLIC_BASE_URL) {
url = `${process.env.STORAGE_PUBLIC_BASE_URL}/${filename}`;
} else if (process.env.STORAGE_ENDPOINT) {
// Basic fallback construction if base url is not set but endpoint is
// This assumes path style access is okay if no custom domain
const endpoint = process.env.STORAGE_ENDPOINT.replace(/\/+$/, '');
url = `${endpoint}/${bucket}/${filename}`;
} else {
console.warn('Storage public URL not configured properly');
return null;
}
return url;
} catch (error) {
console.error('Error generating/uploading avatar:', error);
return null;
}
}
+8 -1
View File
@@ -10,6 +10,7 @@ import { generateKeyPair } from '@/lib/crypto/keys';
import { encryptPrivateKey, serializeEncryptedKey } from '@/lib/crypto/private-key';
import { cookies } from 'next/headers';
import { upsertHandleEntries } from '@/lib/federation/handles';
import { generateAndUploadAvatar } from '@/lib/auth/avatar';
const SESSION_COOKIE_NAME = 'synapsis_session';
const SESSION_EXPIRY_DAYS = 30;
@@ -157,17 +158,23 @@ export async function registerUser(
const did = generateDID();
const passwordHash = await hashPassword(password);
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
// Generate avatar using full handle (@user@domain) for global uniqueness
const fullHandle = `${handle.toLowerCase()}@${nodeDomain}`;
const avatarUrl = await generateAndUploadAvatar(fullHandle);
const [user] = await db.insert(users).values({
did,
handle: handle.toLowerCase(),
email: email.toLowerCase(),
passwordHash,
displayName: displayName || handle,
avatarUrl,
publicKey,
privateKeyEncrypted: serializeEncryptedKey(encryptedPrivateKey),
}).returning();
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
await upsertHandleEntries([{
handle: user.handle,
did: user.did,