feat: handle avatar, first name and last name
This commit is contained in:
parent
86c0a03bfc
commit
a5fcf38d6d
@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "UploadedFile" ADD COLUMN "height" INTEGER,
|
||||||
|
ADD COLUMN "thumbhash" TEXT,
|
||||||
|
ADD COLUMN "width" INTEGER;
|
@ -212,6 +212,9 @@ model UploadedFile {
|
|||||||
accountId String
|
accountId String
|
||||||
account Account @relation(fields: [accountId], references: [id])
|
account Account @relation(fields: [accountId], references: [id])
|
||||||
path String
|
path String
|
||||||
|
width Int?
|
||||||
|
height Int?
|
||||||
|
thumbhash String?
|
||||||
reuseKey String?
|
reuseKey String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
@ -14,10 +14,6 @@ import { auth } from "@/modules/auth";
|
|||||||
import {
|
import {
|
||||||
EventRouter,
|
EventRouter,
|
||||||
ClientConnection,
|
ClientConnection,
|
||||||
SessionScopedConnection,
|
|
||||||
UserScopedConnection,
|
|
||||||
MachineScopedConnection,
|
|
||||||
RecipientFilter,
|
|
||||||
buildNewSessionUpdate,
|
buildNewSessionUpdate,
|
||||||
buildNewMessageUpdate,
|
buildNewMessageUpdate,
|
||||||
buildUpdateSessionUpdate,
|
buildUpdateSessionUpdate,
|
||||||
@ -26,8 +22,6 @@ import {
|
|||||||
buildSessionActivityEphemeral,
|
buildSessionActivityEphemeral,
|
||||||
buildMachineActivityEphemeral,
|
buildMachineActivityEphemeral,
|
||||||
buildUsageEphemeral,
|
buildUsageEphemeral,
|
||||||
buildMachineStatusEphemeral,
|
|
||||||
buildUpdateAccountGithubUpdate
|
|
||||||
} from "@/modules/eventRouter";
|
} from "@/modules/eventRouter";
|
||||||
import {
|
import {
|
||||||
incrementWebSocketConnection,
|
incrementWebSocketConnection,
|
||||||
@ -41,6 +35,8 @@ import {
|
|||||||
import { activityCache } from "@/modules/sessionCache";
|
import { activityCache } from "@/modules/sessionCache";
|
||||||
import { encryptBytes, encryptString } from "@/modules/encrypt";
|
import { encryptBytes, encryptString } from "@/modules/encrypt";
|
||||||
import { GitHubProfile } from "./types";
|
import { GitHubProfile } from "./types";
|
||||||
|
import { uploadImage } from "@/storage/uploadImage";
|
||||||
|
import { separateName } from "@/utils/separateName";
|
||||||
|
|
||||||
|
|
||||||
declare module 'fastify' {
|
declare module 'fastify' {
|
||||||
@ -196,7 +192,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
|||||||
app.addHook('preHandler', async (request, reply) => {
|
app.addHook('preHandler', async (request, reply) => {
|
||||||
// Store original reply.send to catch errors in response serialization
|
// Store original reply.send to catch errors in response serialization
|
||||||
const originalSend = reply.send.bind(reply);
|
const originalSend = reply.send.bind(reply);
|
||||||
reply.send = function(payload: any) {
|
reply.send = function (payload: any) {
|
||||||
try {
|
try {
|
||||||
return originalSend(payload);
|
return originalSend(payload);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -482,15 +478,28 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Avatar
|
||||||
|
const image = await fetch(userData.avatar_url);
|
||||||
|
const imageBuffer = await image.arrayBuffer();
|
||||||
|
const avatar = await uploadImage(userId, 'avatars', 'github', userData.avatar_url, Buffer.from(imageBuffer));
|
||||||
|
|
||||||
|
// Name
|
||||||
|
const name = separateName(userData.name);
|
||||||
|
|
||||||
// Link GitHub user to account
|
// Link GitHub user to account
|
||||||
await db.account.update({
|
await db.account.update({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
data: { githubUserId: githubUser.id }
|
data: { githubUserId: githubUser.id, avatar, firstName: name.firstName, lastName: name.lastName }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send account update to all user connections
|
// Send account update to all user connections
|
||||||
const updSeq = await allocateUserSeq(userId);
|
const updSeq = await allocateUserSeq(userId);
|
||||||
const updatePayload = buildUpdateAccountGithubUpdate(userId, userData, updSeq, randomKeyNaked(12));
|
const updatePayload = buildUpdateAccountUpdate(userId, {
|
||||||
|
github: userData,
|
||||||
|
firstName: name.firstName,
|
||||||
|
lastName: name.lastName,
|
||||||
|
avatar: avatar
|
||||||
|
}, updSeq, randomKeyNaked(12));
|
||||||
eventRouter.emitUpdate({
|
eventRouter.emitUpdate({
|
||||||
userId,
|
userId,
|
||||||
payload: updatePayload,
|
payload: updatePayload,
|
||||||
@ -1112,7 +1121,7 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Send account update to all user connections
|
// Send account update to all user connections
|
||||||
const updatePayload = buildUpdateAccountUpdate(userId, settingsUpdate, updSeq, randomKeyNaked(12));
|
const updatePayload = buildUpdateAccountUpdate(userId, { settings: settingsUpdate }, updSeq, randomKeyNaked(12));
|
||||||
eventRouter.emitUpdate({
|
eventRouter.emitUpdate({
|
||||||
userId,
|
userId,
|
||||||
payload: updatePayload,
|
payload: updatePayload,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Socket } from "socket.io";
|
import { Socket } from "socket.io";
|
||||||
import { log } from "@/utils/log";
|
import { log } from "@/utils/log";
|
||||||
import { GitHubProfile } from "@/app/types";
|
import { GitHubProfile } from "@/app/types";
|
||||||
|
import { AccountProfile } from "@/types";
|
||||||
|
|
||||||
// === CONNECTION TYPES ===
|
// === CONNECTION TYPES ===
|
||||||
|
|
||||||
@ -330,27 +331,14 @@ export function buildUpdateSessionUpdate(sessionId: string, updateSeq: number, u
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildUpdateAccountUpdate(userId: string, settings: { value: string | null; version: number }, updateSeq: number, updateId: string): UpdatePayload {
|
export function buildUpdateAccountUpdate(userId: string, profile: Partial<AccountProfile>, updateSeq: number, updateId: string): UpdatePayload {
|
||||||
return {
|
return {
|
||||||
id: updateId,
|
id: updateId,
|
||||||
seq: updateSeq,
|
seq: updateSeq,
|
||||||
body: {
|
body: {
|
||||||
t: 'update-account',
|
t: 'update-account',
|
||||||
id: userId,
|
id: userId,
|
||||||
settings
|
...profile
|
||||||
},
|
|
||||||
createdAt: Date.now()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildUpdateAccountGithubUpdate(userId: string, github: GitHubProfile | null, updateSeq: number, updateId: string): UpdatePayload {
|
|
||||||
return {
|
|
||||||
id: updateId,
|
|
||||||
seq: updateSeq,
|
|
||||||
body: {
|
|
||||||
t: 'update-account',
|
|
||||||
id: userId,
|
|
||||||
github
|
|
||||||
},
|
},
|
||||||
createdAt: Date.now()
|
createdAt: Date.now()
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,25 @@ import { processImage } from "./processImage";
|
|||||||
import { s3bucket, s3client, s3host } from "./files";
|
import { s3bucket, s3client, s3host } from "./files";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
|
|
||||||
export async function uploadImage(userId: string, directory: string, prefix: string, src: Buffer) {
|
export async function uploadImage(userId: string, directory: string, prefix: string, url: string, src: Buffer) {
|
||||||
|
|
||||||
|
// Check if image already exists
|
||||||
|
const existing = await db.uploadedFile.findFirst({
|
||||||
|
where: {
|
||||||
|
reuseKey: 'image-url:' + url
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing && existing.thumbhash && existing.width && existing.height) {
|
||||||
|
return {
|
||||||
|
path: existing.path,
|
||||||
|
thumbhash: existing.thumbhash,
|
||||||
|
width: existing.width,
|
||||||
|
height: existing.height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process image
|
||||||
const processed = await processImage(src);
|
const processed = await processImage(src);
|
||||||
const key = randomKey(prefix);
|
const key = randomKey(prefix);
|
||||||
let filename = `${key}.${processed.format === 'png' ? 'png' : 'jpg'}`;
|
let filename = `${key}.${processed.format === 'png' ? 'png' : 'jpg'}`;
|
||||||
@ -11,7 +29,11 @@ export async function uploadImage(userId: string, directory: string, prefix: str
|
|||||||
await db.uploadedFile.create({
|
await db.uploadedFile.create({
|
||||||
data: {
|
data: {
|
||||||
accountId: userId,
|
accountId: userId,
|
||||||
path: `user/${userId}/${directory}/${filename}`
|
path: `user/${userId}/${directory}/${filename}`,
|
||||||
|
reuseKey: 'image-url:' + url,
|
||||||
|
width: processed.width,
|
||||||
|
height: processed.height,
|
||||||
|
thumbhash: processed.thumbhash
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
13
sources/types.ts
Normal file
13
sources/types.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { GitHubProfile } from "./app/types";
|
||||||
|
import { ImageRef } from "./storage/files";
|
||||||
|
|
||||||
|
export type AccountProfile = {
|
||||||
|
firstName: string | null;
|
||||||
|
lastName: string | null;
|
||||||
|
avatar: ImageRef | null;
|
||||||
|
github: GitHubProfile | null;
|
||||||
|
settings: {
|
||||||
|
value: string | null;
|
||||||
|
version: number;
|
||||||
|
} | null;
|
||||||
|
}
|
59
sources/utils/separateName.spec.ts
Normal file
59
sources/utils/separateName.spec.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { separateName } from './separateName';
|
||||||
|
|
||||||
|
describe('separateName', () => {
|
||||||
|
it('should separate basic first and last name', () => {
|
||||||
|
const result = separateName('John Doe');
|
||||||
|
expect(result).toEqual({ firstName: 'John', lastName: 'Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single name with no last name', () => {
|
||||||
|
const result = separateName('John');
|
||||||
|
expect(result).toEqual({ firstName: 'John', lastName: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple names putting everything after first as lastName', () => {
|
||||||
|
const result = separateName('John William Doe Smith');
|
||||||
|
expect(result).toEqual({ firstName: 'John', lastName: 'William Doe Smith' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty string', () => {
|
||||||
|
const result = separateName('');
|
||||||
|
expect(result).toEqual({ firstName: null, lastName: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null input', () => {
|
||||||
|
const result = separateName(null);
|
||||||
|
expect(result).toEqual({ firstName: null, lastName: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined input', () => {
|
||||||
|
const result = separateName(undefined);
|
||||||
|
expect(result).toEqual({ firstName: null, lastName: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle whitespace-only string', () => {
|
||||||
|
const result = separateName(' ');
|
||||||
|
expect(result).toEqual({ firstName: null, lastName: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle extra spaces between names', () => {
|
||||||
|
const result = separateName(' John Doe ');
|
||||||
|
expect(result).toEqual({ firstName: 'John', lastName: 'Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle names with special characters', () => {
|
||||||
|
const result = separateName('José María');
|
||||||
|
expect(result).toEqual({ firstName: 'José', lastName: 'María' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle hyphenated last names', () => {
|
||||||
|
const result = separateName('Mary Smith-Johnson');
|
||||||
|
expect(result).toEqual({ firstName: 'Mary', lastName: 'Smith-Johnson' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple middle names and hyphenated last name', () => {
|
||||||
|
const result = separateName('John Michael Robert Smith-Johnson');
|
||||||
|
expect(result).toEqual({ firstName: 'John', lastName: 'Michael Robert Smith-Johnson' });
|
||||||
|
});
|
||||||
|
});
|
27
sources/utils/separateName.ts
Normal file
27
sources/utils/separateName.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
interface NameParts {
|
||||||
|
firstName: string | null;
|
||||||
|
lastName: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function separateName(fullName: string | null | undefined): NameParts {
|
||||||
|
if (!fullName || typeof fullName !== 'string') {
|
||||||
|
return { firstName: null, lastName: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedName = fullName.trim();
|
||||||
|
|
||||||
|
if (!trimmedName) {
|
||||||
|
return { firstName: null, lastName: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = trimmedName.split(/\s+/);
|
||||||
|
|
||||||
|
if (parts.length === 1) {
|
||||||
|
return { firstName: parts[0], lastName: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstName = parts[0];
|
||||||
|
const lastName = parts.slice(1).join(' ');
|
||||||
|
|
||||||
|
return { firstName, lastName };
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user