feat: integrate Kakao OAuth for user authentication and enhance user repository

- Added Kakao OAuth client implementation for handling authentication flows.
- Updated user repository to support OAuth-based user creation and retrieval.
- Introduced Redis-based session management for OAuth sessions.
- Enhanced in-memory user repository to handle OAuth user data.
- Updated environment configuration files to include new OAuth settings.
- Modified API routes to support Kakao login and registration flows.
- Added Prisma schema updates for user model to accommodate OAuth fields.
- Implemented tests for the new authentication flow and user repository methods.
This commit is contained in:
2025-12-30 02:19:32 +00:00
parent f01a21f2f0
commit 0c1e27ee4a
17 changed files with 893 additions and 6 deletions
@@ -0,0 +1,125 @@
import { Prisma, type PrismaClient } from '@prisma/client';
import { createSimplePasswordHasher, type PasswordHasher } from './passwordHasher.js';
import type {
CreateUserInput,
UserOAuthInfo,
UserRecord,
UserRepository,
UserSanctions,
} from './userRepository.js';
const readStringArray = (value: unknown): string[] => {
if (!Array.isArray(value)) {
return [];
}
return value.filter((item): item is string => typeof item === 'string');
};
const readObject = <T extends object>(value: unknown, fallback: T): T => {
if (!value || typeof value !== 'object') {
return fallback;
}
return value as T;
};
const mapUser = (row: {
id: string;
loginId: string;
displayName: string;
passwordHash: string;
passwordSalt: string;
roles: Prisma.JsonValue;
sanctions: Prisma.JsonValue;
oauthType: 'NONE' | 'KAKAO';
oauthId: string | null;
email: string | null;
oauthInfo: Prisma.JsonValue;
createdAt: Date;
}): UserRecord => ({
id: row.id,
username: row.loginId,
displayName: row.displayName,
roles: readStringArray(row.roles),
sanctions: readObject<UserSanctions>(row.sanctions, {}),
oauthType: row.oauthType,
oauthId: row.oauthId ?? undefined,
email: row.email ?? undefined,
oauthInfo: readObject<UserOAuthInfo>(row.oauthInfo, {}),
passwordHash: row.passwordHash,
passwordSalt: row.passwordSalt,
createdAt: row.createdAt.toISOString(),
});
export const createPostgresUserRepository = (
prisma: PrismaClient,
hasher: PasswordHasher = createSimplePasswordHasher()
): UserRepository => {
return {
async findByUsername(username: string): Promise<UserRecord | null> {
const row = await prisma.appUser.findUnique({
where: {
loginId: username,
},
});
return row ? mapUser(row) : null;
},
async findByOauthId(type: 'KAKAO', oauthId: string): Promise<UserRecord | null> {
const row = await prisma.appUser.findFirst({
where: {
oauthType: type,
oauthId,
},
});
return row ? mapUser(row) : null;
},
async findByEmail(email: string): Promise<UserRecord | null> {
const row = await prisma.appUser.findUnique({
where: {
email: email.toLowerCase(),
},
});
return row ? mapUser(row) : null;
},
async createUser(input: CreateUserInput): Promise<UserRecord> {
const salt = hasher.createSalt();
const oauthType = input.oauth?.type ?? 'NONE';
const row = await prisma.appUser.create({
data: {
loginId: input.username,
displayName: input.displayName ?? input.username,
passwordHash: hasher.hash(input.password, salt),
passwordSalt: salt,
roles: ['user'] satisfies Prisma.JsonArray,
sanctions: {} satisfies Prisma.JsonObject,
oauthType,
oauthId: input.oauth?.id,
email: input.oauth?.email?.toLowerCase(),
oauthInfo: (input.oauth?.info ?? {}) as Prisma.JsonObject,
},
});
return mapUser(row);
},
async verifyPassword(user: UserRecord, password: string): Promise<boolean> {
return hasher.hash(password, user.passwordSalt) === user.passwordHash;
},
async updatePassword(userId: string, password: string): Promise<void> {
const salt = hasher.createSalt();
await prisma.appUser.update({
where: { id: userId },
data: {
passwordHash: hasher.hash(password, salt),
passwordSalt: salt,
},
});
},
async updateOAuthInfo(userId: string, oauthInfo: UserOAuthInfo): Promise<void> {
await prisma.appUser.update({
where: { id: userId },
data: {
oauthInfo: oauthInfo as Prisma.JsonObject,
},
});
},
};
};