108 lines
3.9 KiB
TypeScript
108 lines
3.9 KiB
TypeScript
import { db } from "@/storage/db";
|
|
import { Context } from "@/context";
|
|
import { encryptString } from "@/modules/encrypt";
|
|
import { uploadImage } from "@/storage/uploadImage";
|
|
import { separateName } from "@/utils/separateName";
|
|
import { GitHubProfile } from "@/app/api/types";
|
|
import { allocateUserSeq } from "@/storage/seq";
|
|
import { buildUpdateAccountUpdate, eventRouter } from "@/app/events/eventRouter";
|
|
import { randomKeyNaked } from "@/utils/randomKeyNaked";
|
|
import { githubDisconnect } from "./githubDisconnect";
|
|
|
|
/**
|
|
* Connects a GitHub account to a user profile.
|
|
*
|
|
* Flow:
|
|
* 1. Check if already connected to same account - early exit if yes
|
|
* 2. If GitHub account is connected to another user - disconnect it first
|
|
* 3. Upload avatar to S3 (non-transactional operation)
|
|
* 4. In transaction: persist GitHub account and link to user with GitHub username
|
|
* 5. Send socket update after transaction completes
|
|
*
|
|
* @param ctx - Request context containing user ID
|
|
* @param githubProfile - GitHub profile data from OAuth
|
|
* @param accessToken - GitHub access token for API access
|
|
*/
|
|
export async function githubConnect(
|
|
ctx: Context,
|
|
githubProfile: GitHubProfile,
|
|
accessToken: string
|
|
): Promise<void> {
|
|
const userId = ctx.uid;
|
|
const githubUserId = githubProfile.id.toString();
|
|
|
|
// Step 1: Check if user is already connected to this exact GitHub account
|
|
const currentUser = await db.account.findFirstOrThrow({
|
|
where: { id: userId },
|
|
select: { githubUserId: true, username: true }
|
|
});
|
|
if (currentUser.githubUserId === githubUserId) {
|
|
return;
|
|
}
|
|
|
|
// Step 2: Check if GitHub account is connected to another user
|
|
const existingConnection = await db.account.findFirst({
|
|
where: {
|
|
githubUserId: githubUserId,
|
|
NOT: { id: userId }
|
|
}
|
|
});
|
|
if (existingConnection) {
|
|
const disconnectCtx: Context = Context.create(existingConnection.id);
|
|
await githubDisconnect(disconnectCtx);
|
|
}
|
|
|
|
// Step 3: Upload avatar to S3 (outside transaction for performance)
|
|
const imageResponse = await fetch(githubProfile.avatar_url);
|
|
const imageBuffer = await imageResponse.arrayBuffer();
|
|
const avatar = await uploadImage(userId, 'avatars', 'github', githubProfile.avatar_url, Buffer.from(imageBuffer));
|
|
|
|
// Extract name from GitHub profile
|
|
const name = separateName(githubProfile.name);
|
|
|
|
// Step 4: Start transaction for atomic database operations
|
|
await db.$transaction(async (tx) => {
|
|
|
|
// Upsert GitHub user record with encrypted token
|
|
await tx.githubUser.upsert({
|
|
where: { id: githubUserId },
|
|
update: {
|
|
profile: githubProfile,
|
|
token: encryptString(['user', userId, 'github', 'token'], accessToken)
|
|
},
|
|
create: {
|
|
id: githubUserId,
|
|
profile: githubProfile,
|
|
token: encryptString(['user', userId, 'github', 'token'], accessToken)
|
|
}
|
|
});
|
|
|
|
// Link GitHub account to user
|
|
await tx.account.update({
|
|
where: { id: userId },
|
|
data: {
|
|
githubUserId: githubUserId,
|
|
username: githubProfile.login,
|
|
firstName: name.firstName,
|
|
lastName: name.lastName,
|
|
avatar: avatar
|
|
}
|
|
});
|
|
});
|
|
|
|
// Step 5: Send update via socket (after transaction completes)
|
|
const updSeq = await allocateUserSeq(userId);
|
|
const updatePayload = buildUpdateAccountUpdate(userId, {
|
|
github: githubProfile,
|
|
username: githubProfile.login,
|
|
firstName: name.firstName,
|
|
lastName: name.lastName,
|
|
avatar: avatar
|
|
}, updSeq, randomKeyNaked(12));
|
|
|
|
eventRouter.emitUpdate({
|
|
userId,
|
|
payload: updatePayload,
|
|
recipientFilter: { type: 'all-user-authenticated-connections' }
|
|
});
|
|
} |