feat:profile pictures
This commit is contained in:
@@ -4,6 +4,7 @@ import { sha256 } from '@oslojs/crypto/sha2';
|
||||
import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding';
|
||||
import { db } from '$lib/server/db';
|
||||
import * as table from '$lib/server/db/schema';
|
||||
import { getSignedR2Url } from '$lib/server/r2';
|
||||
|
||||
const DAY_IN_MS = 1000 * 60 * 60 * 24;
|
||||
|
||||
@@ -31,7 +32,11 @@ export async function validateSessionToken(token: string) {
|
||||
const [result] = await db
|
||||
.select({
|
||||
// Adjust user table here to tweak returned data
|
||||
user: { id: table.user.id, username: table.user.username },
|
||||
user: {
|
||||
id: table.user.id,
|
||||
username: table.user.username,
|
||||
profilePictureUrl: table.user.profilePictureUrl
|
||||
},
|
||||
session: table.session
|
||||
})
|
||||
.from(table.session)
|
||||
@@ -58,7 +63,25 @@ export async function validateSessionToken(token: string) {
|
||||
.where(eq(table.session.id, session.id));
|
||||
}
|
||||
|
||||
return { session, user };
|
||||
// Generate signed URL for profile picture if it exists
|
||||
let profilePictureUrl = user.profilePictureUrl;
|
||||
if (profilePictureUrl && !profilePictureUrl.startsWith('http')) {
|
||||
// It's a path, generate signed URL
|
||||
try {
|
||||
profilePictureUrl = await getSignedR2Url(profilePictureUrl, 24 * 60 * 60); // 24 hours
|
||||
} catch (error) {
|
||||
console.error('Failed to generate signed URL for profile picture:', error);
|
||||
profilePictureUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
user: {
|
||||
...user,
|
||||
profilePictureUrl
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type SessionValidationResult = Awaited<ReturnType<typeof validateSessionToken>>;
|
||||
@@ -95,7 +118,8 @@ export async function createUser(googleId: string, name: string) {
|
||||
username: name,
|
||||
googleId,
|
||||
passwordHash: null,
|
||||
age: null
|
||||
age: null,
|
||||
profilePictureUrl: null
|
||||
};
|
||||
await db.insert(table.user).values(user);
|
||||
return user;
|
||||
|
||||
@@ -5,7 +5,8 @@ export const user = pgTable('user', {
|
||||
age: integer('age'),
|
||||
username: text('username').notNull().unique(),
|
||||
passwordHash: text('password_hash'),
|
||||
googleId: text('google_id').unique()
|
||||
googleId: text('google_id').unique(),
|
||||
profilePictureUrl: text('profile_picture_url')
|
||||
});
|
||||
|
||||
export const session = pgTable('session', {
|
||||
|
||||
@@ -123,3 +123,85 @@ export async function processAndUploadVideo(
|
||||
fallbackThumbnailUrl: thumbnailUrl
|
||||
};
|
||||
}
|
||||
|
||||
export async function processAndUploadProfilePicture(
|
||||
file: File,
|
||||
userId: string
|
||||
): Promise<{
|
||||
url: string;
|
||||
thumbnailUrl: string;
|
||||
fallbackUrl?: string;
|
||||
fallbackThumbnailUrl?: string;
|
||||
}> {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
// Generate unique filename
|
||||
const timestamp = Date.now();
|
||||
const randomId = Math.random().toString(36).substring(2, 15);
|
||||
const filename = `users/${userId}/profile-${timestamp}-${randomId}`;
|
||||
|
||||
// Process full-size image in WebP format (with JPEG fallback)
|
||||
const processedWebP = await sharp(buffer)
|
||||
.resize(400, 400, {
|
||||
fit: 'cover',
|
||||
position: 'centre'
|
||||
})
|
||||
.webp({ quality: 85, effort: 4 })
|
||||
.toBuffer();
|
||||
|
||||
// Generate JPEG fallback for older browsers
|
||||
const processedJPEG = await sharp(buffer)
|
||||
.resize(400, 400, {
|
||||
fit: 'cover',
|
||||
position: 'centre'
|
||||
})
|
||||
.jpeg({ quality: 85, progressive: true })
|
||||
.toBuffer();
|
||||
|
||||
// Generate smaller thumbnail in WebP format
|
||||
const thumbnailWebP = await sharp(buffer)
|
||||
.resize(150, 150, {
|
||||
fit: 'cover',
|
||||
position: 'centre'
|
||||
})
|
||||
.webp({ quality: 80, effort: 4 })
|
||||
.toBuffer();
|
||||
|
||||
// Generate JPEG thumbnail fallback
|
||||
const thumbnailJPEG = await sharp(buffer)
|
||||
.resize(150, 150, {
|
||||
fit: 'cover',
|
||||
position: 'centre'
|
||||
})
|
||||
.jpeg({ quality: 80 })
|
||||
.toBuffer();
|
||||
|
||||
// Upload all variants to R2
|
||||
const webpFile = new File([new Uint8Array(processedWebP)], `${filename}.webp`, {
|
||||
type: 'image/webp'
|
||||
});
|
||||
const jpegFile = new File([new Uint8Array(processedJPEG)], `${filename}.jpg`, {
|
||||
type: 'image/jpeg'
|
||||
});
|
||||
const thumbWebPFile = new File([new Uint8Array(thumbnailWebP)], `${filename}-thumb.webp`, {
|
||||
type: 'image/webp'
|
||||
});
|
||||
const thumbJPEGFile = new File([new Uint8Array(thumbnailJPEG)], `${filename}-thumb.jpg`, {
|
||||
type: 'image/jpeg'
|
||||
});
|
||||
|
||||
const [webpPath, jpegPath, thumbWebPPath, thumbJPEGPath] = await Promise.all([
|
||||
uploadToR2(webpFile, `${filename}.webp`, 'image/webp'),
|
||||
uploadToR2(jpegFile, `${filename}.jpg`, 'image/jpeg'),
|
||||
uploadToR2(thumbWebPFile, `${filename}-thumb.webp`, 'image/webp'),
|
||||
uploadToR2(thumbJPEGFile, `${filename}-thumb.jpg`, 'image/jpeg')
|
||||
]);
|
||||
|
||||
// Return WebP URLs as primary, JPEG as fallback (client can choose based on browser support)
|
||||
return {
|
||||
url: webpPath,
|
||||
thumbnailUrl: thumbWebPPath,
|
||||
fallbackUrl: jpegPath,
|
||||
fallbackThumbnailUrl: thumbJPEGPath
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user