mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Fixing OAuth flows, passing code and state correctly to hono, and signing in with GitHub.
This commit is contained in:
parent
fbf4d08b07
commit
a0b01e5ade
16 changed files with 319 additions and 233 deletions
|
|
@ -4,52 +4,73 @@ import { LuciaService } from '$lib/server/api/services/lucia.service'
|
||||||
import { OAuthService } from '$lib/server/api/services/oauth.service'
|
import { OAuthService } from '$lib/server/api/services/oauth.service'
|
||||||
import { github } from '$lib/server/auth'
|
import { github } from '$lib/server/auth'
|
||||||
import { OAuth2RequestError } from 'arctic'
|
import { OAuth2RequestError } from 'arctic'
|
||||||
import { getCookie } from 'hono/cookie'
|
import { getCookie, setCookie } from 'hono/cookie'
|
||||||
|
import { TimeSpan } from 'oslo'
|
||||||
import { inject, injectable } from 'tsyringe'
|
import { inject, injectable } from 'tsyringe'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class OAuthController extends Controller {
|
export class OAuthController extends Controller {
|
||||||
constructor(
|
constructor(
|
||||||
@inject(LuciaService) private luciaService: LuciaService,
|
@inject(LuciaService) private luciaService: LuciaService,
|
||||||
@inject(OAuthService) private readonly oauthService: OAuthService) {
|
@inject(OAuthService) private oauthService: OAuthService,
|
||||||
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
routes() {
|
routes() {
|
||||||
return this.controller
|
return this.controller.get('/github', async (c) => {
|
||||||
.get('/github', async (c) => {
|
try {
|
||||||
try {
|
const code = c.req.query('code')?.toString() ?? null
|
||||||
const code = c.req.query('code')?.toString() ?? null
|
const state = c.req.query('state')?.toString() ?? null
|
||||||
const state = c.req.query('state')?.toString() ?? null
|
const storedState = getCookie(c).github_oauth_state ?? null
|
||||||
const storedState = getCookie(c).github_oauth_state ?? null
|
|
||||||
|
|
||||||
if (!code || !state || !storedState || state !== storedState) {
|
console.log('code', code, 'state', state, 'storedState', storedState)
|
||||||
return c.body(null, 400)
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokens = await github.validateAuthorizationCode(code)
|
if (!code || !state || !storedState || state !== storedState) {
|
||||||
const githubUserResponse = await fetch("https://api.github.com/user", {
|
return c.body(null, 400)
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${tokens.accessToken}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const githubUser: GitHubUser = await githubUserResponse.json();
|
|
||||||
|
|
||||||
const token = await this.oauthService.handleOAuthUser(githubUser.id, githubUser.login, 'github')
|
|
||||||
return c.json({ token })
|
|
||||||
} catch (error) {
|
|
||||||
// the specific error message depends on the provider
|
|
||||||
if (error instanceof OAuth2RequestError) {
|
|
||||||
// invalid code
|
|
||||||
return c.body(null, 400)
|
|
||||||
}
|
|
||||||
return c.body(null, 500)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
const tokens = await github.validateAuthorizationCode(code)
|
||||||
|
const githubUserResponse = await fetch('https://api.github.com/user', {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens.accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const githubUser: GitHubUser = await githubUserResponse.json()
|
||||||
|
|
||||||
|
const userId = await this.oauthService.handleOAuthUser(githubUser.id, githubUser.login, 'github')
|
||||||
|
|
||||||
|
const session = await this.luciaService.lucia.createSession(userId, {})
|
||||||
|
const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id)
|
||||||
|
|
||||||
|
setCookie(c, sessionCookie.name, sessionCookie.value, {
|
||||||
|
path: sessionCookie.attributes.path,
|
||||||
|
maxAge:
|
||||||
|
sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds()
|
||||||
|
? sessionCookie.attributes.maxAge
|
||||||
|
: new TimeSpan(2, 'w').seconds(),
|
||||||
|
domain: sessionCookie.attributes.domain,
|
||||||
|
sameSite: sessionCookie.attributes.sameSite as any,
|
||||||
|
secure: sessionCookie.attributes.secure,
|
||||||
|
httpOnly: sessionCookie.attributes.httpOnly,
|
||||||
|
expires: sessionCookie.attributes.expires,
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.json({ message: 'ok' })
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
// the specific error message depends on the provider
|
||||||
|
if (error instanceof OAuth2RequestError) {
|
||||||
|
// invalid code
|
||||||
|
return c.body(null, 400)
|
||||||
|
}
|
||||||
|
return c.body(null, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GitHubUser {
|
interface GitHubUser {
|
||||||
id: number;
|
id: number
|
||||||
login: string;
|
login: string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'reflect-metadata'
|
import 'reflect-metadata'
|
||||||
import { CollectionController } from '$lib/server/api/controllers/collection.controller'
|
import { CollectionController } from '$lib/server/api/controllers/collection.controller'
|
||||||
import { MfaController } from '$lib/server/api/controllers/mfa.controller'
|
import { MfaController } from '$lib/server/api/controllers/mfa.controller'
|
||||||
|
import { OAuthController } from '$lib/server/api/controllers/oauth.controller'
|
||||||
import { SignupController } from '$lib/server/api/controllers/signup.controller'
|
import { SignupController } from '$lib/server/api/controllers/signup.controller'
|
||||||
import { UserController } from '$lib/server/api/controllers/user.controller'
|
import { UserController } from '$lib/server/api/controllers/user.controller'
|
||||||
import { WishlistController } from '$lib/server/api/controllers/wishlist.controller'
|
import { WishlistController } from '$lib/server/api/controllers/wishlist.controller'
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { takeFirstOrThrow } from '$lib/server/api/common/utils/repository'
|
||||||
import { DrizzleService } from '$lib/server/api/services/drizzle.service'
|
import { DrizzleService } from '$lib/server/api/services/drizzle.service'
|
||||||
import { type InferInsertModel, eq } from 'drizzle-orm'
|
import { type InferInsertModel, eq } from 'drizzle-orm'
|
||||||
import { inject, injectable } from 'tsyringe'
|
import { inject, injectable } from 'tsyringe'
|
||||||
import { collection_items, collections } from '../databases/tables'
|
import { collections } from '../databases/tables'
|
||||||
|
|
||||||
export type CreateCollection = InferInsertModel<typeof collections>
|
export type CreateCollection = InferInsertModel<typeof collections>
|
||||||
export type UpdateCollection = Partial<CreateCollection>
|
export type UpdateCollection = Partial<CreateCollection>
|
||||||
|
|
@ -61,10 +61,10 @@ export class CollectionsRepository {
|
||||||
with: {
|
with: {
|
||||||
collection_items: {
|
collection_items: {
|
||||||
columns: {
|
columns: {
|
||||||
cuid: true
|
cuid: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export class CredentialsRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
|
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
|
||||||
const credentials = await this.findOneById(id)
|
const credentials = await this.findOneById(id, db)
|
||||||
if (!credentials) throw Error('Credentials not found')
|
if (!credentials) throw Error('Credentials not found')
|
||||||
return credentials
|
return credentials
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,28 @@
|
||||||
import { inject, injectable } from "tsyringe";
|
import { type InferInsertModel, and, eq } from 'drizzle-orm'
|
||||||
import { DrizzleService } from "../services/drizzle.service";
|
import { inject, injectable } from 'tsyringe'
|
||||||
import { and, eq, type InferInsertModel } from "drizzle-orm";
|
import { takeFirstOrThrow } from '../common/utils/repository'
|
||||||
import { federatedIdentityTable } from "../databases/tables";
|
import { federatedIdentityTable } from '../databases/tables'
|
||||||
import { takeFirstOrThrow } from "../common/utils/repository";
|
import { DrizzleService } from '../services/drizzle.service'
|
||||||
|
|
||||||
export type CreateFederatedIdentity = InferInsertModel<typeof federatedIdentityTable>
|
export type CreateFederatedIdentity = InferInsertModel<typeof federatedIdentityTable>
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class FederatedIdentityRepository {
|
export class FederatedIdentityRepository {
|
||||||
|
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {}
|
||||||
constructor(
|
|
||||||
@inject(DrizzleService) private readonly drizzle: DrizzleService
|
|
||||||
) { }
|
|
||||||
|
|
||||||
async findOneByUserIdAndProvider(userId: string, provider: string) {
|
async findOneByUserIdAndProvider(userId: string, provider: string) {
|
||||||
return this.drizzle.db.query.federatedIdentityTable.findFirst({
|
return this.drizzle.db.query.federatedIdentityTable.findFirst({
|
||||||
where: and(eq(federatedIdentityTable.user_id, userId), eq(federatedIdentityTable.identity_provider, provider))
|
where: and(eq(federatedIdentityTable.user_id, userId), eq(federatedIdentityTable.identity_provider, provider)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneByFederatedUserIdAndProvider(federatedUserId: string, provider: string) {
|
||||||
|
return this.drizzle.db.query.federatedIdentityTable.findFirst({
|
||||||
|
where: and(eq(federatedIdentityTable.federated_user_id, federatedUserId), eq(federatedIdentityTable.identity_provider, provider)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(data: CreateFederatedIdentity, db = this.drizzle.db) {
|
async create(data: CreateFederatedIdentity, db = this.drizzle.db) {
|
||||||
return db.insert(federatedIdentityTable).values(data).returning().then(takeFirstOrThrow)
|
return db.insert(federatedIdentityTable).values(data).returning().then(takeFirstOrThrow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,29 +28,29 @@ export class RolesRepository {
|
||||||
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {}
|
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {}
|
||||||
|
|
||||||
async findOneById(id: string, db = this.drizzle.db) {
|
async findOneById(id: string, db = this.drizzle.db) {
|
||||||
return db.query.roles.findFirst({
|
return db.query.rolesTable.findFirst({
|
||||||
where: eq(rolesTable.id, id),
|
where: eq(rolesTable.id, id),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
|
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
|
||||||
const role = await this.findOneById(id)
|
const role = await this.findOneById(id, db)
|
||||||
if (!role) throw Error('Role not found')
|
if (!role) throw Error('Role not found')
|
||||||
return role
|
return role
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(db = this.drizzle.db) {
|
async findAll(db = this.drizzle.db) {
|
||||||
return db.query.roles.findMany()
|
return db.query.rolesTable.findMany()
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByName(name: string, db = this.drizzle.db) {
|
async findOneByName(name: string, db = this.drizzle.db) {
|
||||||
return db.query.roles.findFirst({
|
return db.query.rolesTable.findFirst({
|
||||||
where: eq(rolesTable.name, name),
|
where: eq(rolesTable.name, name),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByNameOrThrow(name: string, db = this.drizzle.db) {
|
async findOneByNameOrThrow(name: string, db = this.drizzle.db) {
|
||||||
const role = await this.findOneByName(name)
|
const role = await this.findOneByName(name, db)
|
||||||
if (!role) throw Error('Role not found')
|
if (!role) throw Error('Role not found')
|
||||||
return role
|
return role
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@ export class UserRolesRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByIdOrThrow(id: string) {
|
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
|
||||||
const userRole = await this.findOneById(id)
|
const userRole = await this.findOneById(id, db)
|
||||||
if (!userRole) throw Error('User not found')
|
if (!userRole) throw Error('User not found')
|
||||||
return userRole
|
return userRole
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,50 @@
|
||||||
import { inject, injectable } from "tsyringe";
|
import type { db } from '$lib/server/api/packages/drizzle'
|
||||||
import { generateRandomAnimalName } from "$lib/utils/randomDataUtil";
|
import { generateRandomAnimalName } from '$lib/utils/randomDataUtil'
|
||||||
import { CollectionsRepository } from "../repositories/collections.repository";
|
import { inject, injectable } from 'tsyringe'
|
||||||
|
import { CollectionsRepository } from '../repositories/collections.repository'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class CollectionsService {
|
export class CollectionsService {
|
||||||
constructor(
|
constructor(@inject(CollectionsRepository) private readonly collectionsRepository: CollectionsRepository) {}
|
||||||
@inject(CollectionsRepository) private readonly collectionsRepository: CollectionsRepository
|
|
||||||
) { }
|
|
||||||
|
|
||||||
async findOneByUserId(userId: string) {
|
async findOneByUserId(userId: string) {
|
||||||
return this.collectionsRepository.findOneByUserId(userId);
|
return this.collectionsRepository.findOneByUserId(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAllByUserId(userId: string) {
|
async findAllByUserId(userId: string) {
|
||||||
return this.collectionsRepository.findAllByUserId(userId);
|
return this.collectionsRepository.findAllByUserId(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAllByUserIdWithDetails(userId: string) {
|
async findAllByUserIdWithDetails(userId: string) {
|
||||||
return this.collectionsRepository.findAllByUserIdWithDetails(userId);
|
return this.collectionsRepository.findAllByUserIdWithDetails(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneById(id: string) {
|
async findOneById(id: string) {
|
||||||
return this.collectionsRepository.findOneById(id);
|
return this.collectionsRepository.findOneById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByCuid(cuid: string) {
|
async findOneByCuid(cuid: string) {
|
||||||
return this.collectionsRepository.findOneByCuid(cuid);
|
return this.collectionsRepository.findOneByCuid(cuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
async createEmptyNoName(userId: string) {
|
async createEmptyNoName(userId: string, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
|
||||||
return this.createEmpty(userId, null);
|
return this.createEmpty(userId, null, trx)
|
||||||
}
|
}
|
||||||
|
|
||||||
async createEmpty(userId: string, name: string | null) {
|
async createEmpty(userId: string, name: string | null, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
|
||||||
return this.collectionsRepository.create({
|
if (!trx) {
|
||||||
user_id: userId,
|
return this.collectionsRepository.create({
|
||||||
name: name ?? generateRandomAnimalName(),
|
user_id: userId,
|
||||||
});
|
name: name ?? generateRandomAnimalName(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.collectionsRepository.create(
|
||||||
|
{
|
||||||
|
user_id: userId,
|
||||||
|
name: name ?? generateRandomAnimalName(),
|
||||||
|
},
|
||||||
|
trx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,26 @@
|
||||||
import { github } from "$lib/server/auth";
|
import { inject, injectable } from 'tsyringe'
|
||||||
import { db } from "$lib/server/db";
|
import { FederatedIdentityRepository } from '../repositories/federated_identity.repository'
|
||||||
import { lucia } from "$lib/server/lucia";
|
import { UsersService } from './users.service'
|
||||||
import type { RequestEvent } from "@sveltejs/kit";
|
|
||||||
import { OAuth2RequestError } from "arctic";
|
|
||||||
import { generateIdFromEntropySize } from "lucia";
|
|
||||||
import { inject, injectable } from "tsyringe";
|
|
||||||
import { UsersService } from "./users.service";
|
|
||||||
import { FederatedIdentityRepository } from "../repositories/federated_identity.repository";
|
|
||||||
|
|
||||||
@inejectable()
|
@injectable()
|
||||||
export class OAuthService {
|
export class OAuthService {
|
||||||
constructor(
|
constructor(
|
||||||
@inject(FederatedIdentityRepository) private readonly federatedIdentityRepository: FederatedIdentityRepository,
|
@inject(FederatedIdentityRepository) private readonly federatedIdentityRepository: FederatedIdentityRepository,
|
||||||
@inject(UsersService) private readonly usersService: UsersService
|
@inject(UsersService) private readonly usersService: UsersService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handleOAuthUser(oauthUserId: number, oauthUsername: string, oauthProvider: string) {
|
async handleOAuthUser(oauthUserId: number, oauthUsername: string, oauthProvider: string) {
|
||||||
const existingUser = await this.federatedIdentityRepository.findOneByUserIdAndProvider(`${oauthUserId}`, oauthProvider)
|
const federatedUser = await this.federatedIdentityRepository.findOneByFederatedUserIdAndProvider(`${oauthUserId}`, oauthProvider)
|
||||||
|
|
||||||
if (existingUser) {
|
if (federatedUser) {
|
||||||
return existingUser.id
|
return federatedUser.user_id
|
||||||
} else {
|
|
||||||
const userId = generateIdFromEntropySize(10); // 16 characters long
|
|
||||||
|
|
||||||
const user = await this.drizzleService.db.transaction(async (trx) => {
|
|
||||||
const user = await this.usersService.create({
|
|
||||||
id: userId,
|
|
||||||
username: oauthUsername
|
|
||||||
}, trx);
|
|
||||||
|
|
||||||
await this.federatedIdentityRepository.create({
|
|
||||||
identity_provider: oauthProvider,
|
|
||||||
user_id: userId,
|
|
||||||
identity_id: `${oauthUserId}`
|
|
||||||
}, trx);
|
|
||||||
return user;
|
|
||||||
})
|
|
||||||
|
|
||||||
const session = await lucia.createSession(userId, {});
|
|
||||||
const sessionCookie = lucia.createSessionCookie(session.id);
|
|
||||||
event.cookies.set(sessionCookie.name, sessionCookie.value, {
|
|
||||||
path: ".",
|
|
||||||
...sessionCookie.attributes
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return new Response(null, {
|
|
||||||
status: 302,
|
const user = await this.usersService.createOAuthUser(oauthUserId, oauthUsername, oauthProvider)
|
||||||
headers: {
|
|
||||||
Location: "/"
|
if (!user) {
|
||||||
}
|
throw new Error('Failed to create user')
|
||||||
});
|
}
|
||||||
|
return user.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,51 @@
|
||||||
import {inject, injectable} from "tsyringe";
|
import type { db } from '$lib/server/api/packages/drizzle'
|
||||||
import {type CreateUserRole, UserRolesRepository} from "$lib/server/api/repositories/user_roles.repository";
|
import { type CreateUserRole, UserRolesRepository } from '$lib/server/api/repositories/user_roles.repository'
|
||||||
import {RolesService} from "$lib/server/api/services/roles.service";
|
import { RolesService } from '$lib/server/api/services/roles.service'
|
||||||
|
import { inject, injectable } from 'tsyringe'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class UserRolesService {
|
export class UserRolesService {
|
||||||
constructor(
|
constructor(
|
||||||
@inject(UserRolesRepository) private readonly userRolesRepository: UserRolesRepository,
|
@inject(UserRolesRepository) private readonly userRolesRepository: UserRolesRepository,
|
||||||
@inject(RolesService) private readonly rolesService: RolesService
|
@inject(RolesService) private readonly rolesService: RolesService,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
async findOneById(id: string) {
|
async findOneById(id: string) {
|
||||||
return this.userRolesRepository.findOneById(id);
|
return this.userRolesRepository.findOneById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAllByUserId(userId: string) {
|
async findAllByUserId(userId: string) {
|
||||||
return this.userRolesRepository.findAllByUserId(userId);
|
return this.userRolesRepository.findAllByUserId(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(data: CreateUserRole) {
|
async create(data: CreateUserRole) {
|
||||||
return this.userRolesRepository.create(data);
|
return this.userRolesRepository.create(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
async addRoleToUser(userId: string, roleName: string, primary = false) {
|
async addRoleToUser(userId: string, roleName: string, primary = false, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
|
||||||
// Find the role by its name
|
// Find the role by its name
|
||||||
const role = await this.rolesService.findOneByNameOrThrow(roleName);
|
const role = await this.rolesService.findOneByNameOrThrow(roleName)
|
||||||
|
|
||||||
if (!role || !role.id) {
|
if (!role || !role.id) {
|
||||||
throw new Error(`Role with name ${roleName} not found`);
|
throw new Error(`Role with name ${roleName} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trx) {
|
||||||
|
return this.userRolesRepository.create({
|
||||||
|
user_id: userId,
|
||||||
|
role_id: role.id,
|
||||||
|
primary,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a UserRole entry linking the user and the role
|
// Create a UserRole entry linking the user and the role
|
||||||
return this.userRolesRepository.create({
|
return this.userRolesRepository.create(
|
||||||
user_id: userId,
|
{
|
||||||
role_id: role.id,
|
user_id: userId,
|
||||||
primary,
|
role_id: role.id,
|
||||||
});
|
primary,
|
||||||
|
},
|
||||||
|
trx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import type { SignupUsernameEmailDto } from '$lib/server/api/dtos/signup-username-email.dto'
|
import type { SignupUsernameEmailDto } from '$lib/server/api/dtos/signup-username-email.dto'
|
||||||
import { CredentialsRepository } from '$lib/server/api/repositories/credentials.repository'
|
import { CredentialsRepository } from '$lib/server/api/repositories/credentials.repository'
|
||||||
|
import { FederatedIdentityRepository } from '$lib/server/api/repositories/federated_identity.repository'
|
||||||
|
import { WishlistsRepository } from '$lib/server/api/repositories/wishlists.repository'
|
||||||
import { TokensService } from '$lib/server/api/services/tokens.service'
|
import { TokensService } from '$lib/server/api/services/tokens.service'
|
||||||
import { UserRolesService } from '$lib/server/api/services/user_roles.service'
|
import { UserRolesService } from '$lib/server/api/services/user_roles.service'
|
||||||
import { inject, injectable } from 'tsyringe'
|
import { inject, injectable } from 'tsyringe'
|
||||||
import { CredentialsType } from '../databases/tables'
|
import { CredentialsType } from '../databases/tables'
|
||||||
import { type UpdateUser, UsersRepository } from '../repositories/users.repository'
|
import { type UpdateUser, UsersRepository } from '../repositories/users.repository'
|
||||||
import { CollectionsService } from './collections.service'
|
import { CollectionsService } from './collections.service'
|
||||||
|
import { DrizzleService } from './drizzle.service'
|
||||||
import { WishlistsService } from './wishlists.service'
|
import { WishlistsService } from './wishlists.service'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
|
|
@ -13,9 +16,12 @@ export class UsersService {
|
||||||
constructor(
|
constructor(
|
||||||
@inject(CollectionsService) private readonly collectionsService: CollectionsService,
|
@inject(CollectionsService) private readonly collectionsService: CollectionsService,
|
||||||
@inject(CredentialsRepository) private readonly credentialsRepository: CredentialsRepository,
|
@inject(CredentialsRepository) private readonly credentialsRepository: CredentialsRepository,
|
||||||
|
@inject(DrizzleService) private readonly drizzleService: DrizzleService,
|
||||||
|
@inject(FederatedIdentityRepository) private readonly federatedIdentityRepository: FederatedIdentityRepository,
|
||||||
@inject(TokensService) private readonly tokenService: TokensService,
|
@inject(TokensService) private readonly tokenService: TokensService,
|
||||||
@inject(UsersRepository) private readonly usersRepository: UsersRepository,
|
@inject(UsersRepository) private readonly usersRepository: UsersRepository,
|
||||||
@inject(UserRolesService) private readonly userRolesService: UserRolesService,
|
@inject(UserRolesService) private readonly userRolesService: UserRolesService,
|
||||||
|
@inject(WishlistsRepository) private readonly wishlistsRepository: WishlistsRepository,
|
||||||
@inject(WishlistsService) private readonly wishlistsService: WishlistsService,
|
@inject(WishlistsService) private readonly wishlistsService: WishlistsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
@ -23,34 +29,71 @@ export class UsersService {
|
||||||
const { firstName, lastName, email, username, password } = data
|
const { firstName, lastName, email, username, password } = data
|
||||||
|
|
||||||
const hashedPassword = await this.tokenService.createHashedToken(password)
|
const hashedPassword = await this.tokenService.createHashedToken(password)
|
||||||
const user = await this.usersRepository.create({
|
return await this.drizzleService.db.transaction(async (trx) => {
|
||||||
first_name: firstName,
|
const createdUser = await this.usersRepository.create(
|
||||||
last_name: lastName,
|
{
|
||||||
email,
|
first_name: firstName,
|
||||||
username,
|
last_name: lastName,
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
},
|
||||||
|
trx,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!createdUser) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = await this.credentialsRepository.create(
|
||||||
|
{
|
||||||
|
user_id: createdUser.id,
|
||||||
|
type: CredentialsType.PASSWORD,
|
||||||
|
secret_data: hashedPassword,
|
||||||
|
},
|
||||||
|
trx,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!credentials) {
|
||||||
|
await this.usersRepository.delete(createdUser.id)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userRolesService.addRoleToUser(createdUser.id, 'user', true, trx)
|
||||||
|
|
||||||
|
await this.wishlistsService.createEmptyNoName(createdUser.id, trx)
|
||||||
|
await this.collectionsService.createEmptyNoName(createdUser.id, trx)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!user) {
|
async createOAuthUser(oauthUserId: number, oauthUsername: string, oauthProvider: string) {
|
||||||
return null
|
return await this.drizzleService.db.transaction(async (trx) => {
|
||||||
}
|
const createdUser = await this.usersRepository.create(
|
||||||
|
{
|
||||||
|
username: oauthUsername,
|
||||||
|
},
|
||||||
|
trx,
|
||||||
|
)
|
||||||
|
|
||||||
const credentials = await this.credentialsRepository.create({
|
if (!createdUser) {
|
||||||
user_id: user.id,
|
return null
|
||||||
type: CredentialsType.PASSWORD,
|
}
|
||||||
secret_data: hashedPassword,
|
|
||||||
|
await this.federatedIdentityRepository.create(
|
||||||
|
{
|
||||||
|
identity_provider: oauthProvider,
|
||||||
|
user_id: createdUser.id,
|
||||||
|
federated_user_id: `${oauthUserId}`,
|
||||||
|
federated_username: oauthUsername,
|
||||||
|
},
|
||||||
|
trx,
|
||||||
|
)
|
||||||
|
|
||||||
|
await this.userRolesService.addRoleToUser(createdUser.id, 'user', true, trx)
|
||||||
|
|
||||||
|
await this.wishlistsService.createEmptyNoName(createdUser.id, trx)
|
||||||
|
await this.collectionsService.createEmptyNoName(createdUser.id, trx)
|
||||||
|
return createdUser
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!credentials) {
|
|
||||||
await this.usersRepository.delete(user.id)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.userRolesService.addRoleToUser(user.id, 'user', true)
|
|
||||||
|
|
||||||
await this.wishlistsService.createEmptyNoName(user.id)
|
|
||||||
await this.collectionsService.createEmptyNoName(user.id)
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUser(userId: string, data: UpdateUser) {
|
async updateUser(userId: string, data: UpdateUser) {
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,41 @@
|
||||||
import { inject, injectable } from "tsyringe";
|
import type { db } from '$lib/server/api/packages/drizzle'
|
||||||
import { WishlistsRepository } from "../repositories/wishlists.repository";
|
import { generateRandomAnimalName } from '$lib/utils/randomDataUtil'
|
||||||
import { generateRandomAnimalName } from "$lib/utils/randomDataUtil";
|
import { inject, injectable } from 'tsyringe'
|
||||||
|
import { WishlistsRepository } from '../repositories/wishlists.repository'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class WishlistsService {
|
export class WishlistsService {
|
||||||
|
constructor(@inject(WishlistsRepository) private readonly wishlistsRepository: WishlistsRepository) {}
|
||||||
constructor(
|
|
||||||
@inject(WishlistsRepository) private readonly wishlistsRepository: WishlistsRepository
|
|
||||||
) { }
|
|
||||||
|
|
||||||
async findAllByUserId(userId: string) {
|
async findAllByUserId(userId: string) {
|
||||||
return this.wishlistsRepository.findAllByUserId(userId);
|
return this.wishlistsRepository.findAllByUserId(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneById(id: string) {
|
async findOneById(id: string) {
|
||||||
return this.wishlistsRepository.findOneById(id);
|
return this.wishlistsRepository.findOneById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByCuid(cuid: string) {
|
async findOneByCuid(cuid: string) {
|
||||||
return this.wishlistsRepository.findOneByCuid(cuid);
|
return this.wishlistsRepository.findOneByCuid(cuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
async createEmptyNoName(userId: string) {
|
async createEmptyNoName(userId: string, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
|
||||||
return this.createEmpty(userId, null);
|
return this.createEmpty(userId, null, trx)
|
||||||
}
|
}
|
||||||
|
|
||||||
async createEmpty(userId: string, name: string | null) {
|
async createEmpty(userId: string, name: string | null, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
|
||||||
return this.wishlistsRepository.create({
|
if (!trx) {
|
||||||
user_id: userId,
|
return this.wishlistsRepository.create({
|
||||||
name: name ?? generateRandomAnimalName(),
|
user_id: userId,
|
||||||
});
|
name: name ?? generateRandomAnimalName(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return this.wishlistsRepository.create(
|
||||||
|
{
|
||||||
|
user_id: userId,
|
||||||
|
name: name ?? generateRandomAnimalName(),
|
||||||
|
},
|
||||||
|
trx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,31 @@
|
||||||
import { github } from "$lib/server/auth";
|
import { StatusCodes } from '$lib/constants/status-codes'
|
||||||
import { OAuth2RequestError } from "arctic";
|
import type { RequestEvent } from '@sveltejs/kit'
|
||||||
import { generateIdFromEntropySize } from "lucia";
|
import { redirect } from 'sveltekit-flash-message/server'
|
||||||
|
|
||||||
import type { RequestEvent } from "@sveltejs/kit";
|
|
||||||
|
|
||||||
export async function GET(event: RequestEvent): Promise<Response> {
|
export async function GET(event: RequestEvent): Promise<Response> {
|
||||||
const code = event.url.searchParams.get('code')
|
const { locals, url } = event
|
||||||
const state = event.url.searchParams.get('state')
|
const code = url.searchParams.get('code')
|
||||||
const { data, error } = await locals.api.oauth.$get({
|
const state = url.searchParams.get('state')
|
||||||
params: {
|
console.log('code', code, 'state', state)
|
||||||
code,
|
|
||||||
state
|
const { data, error } = await locals.api.oauth.github.$get({ query: { code, state } }).then(locals.parseApiResponse)
|
||||||
}
|
|
||||||
})
|
if (error) {
|
||||||
|
return new Response(JSON.stringify(error), {
|
||||||
|
status: 400,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return new Response(JSON.stringify({ message: 'Invalid request' }), {
|
||||||
|
status: 400,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(StatusCodes.TEMPORARY_REDIRECT, '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GitHubUser {
|
interface GitHubUser {
|
||||||
id: number;
|
id: number
|
||||||
login: string;
|
login: string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { StatusCodes } from '$lib/constants/status-codes'
|
||||||
import { signinUsernameDto } from '$lib/dtos/signin-username.dto'
|
import { signinUsernameDto } from '$lib/dtos/signin-username.dto'
|
||||||
import { type Actions, fail } from '@sveltejs/kit'
|
import { type Actions, fail } from '@sveltejs/kit'
|
||||||
import { redirect } from 'sveltekit-flash-message/server'
|
import { redirect } from 'sveltekit-flash-message/server'
|
||||||
|
|
@ -136,6 +137,8 @@ export const actions: Actions = {
|
||||||
form.data.username = ''
|
form.data.username = ''
|
||||||
form.data.password = ''
|
form.data.password = ''
|
||||||
|
|
||||||
|
redirect(StatusCodes.TEMPORARY_REDIRECT, '/')
|
||||||
|
|
||||||
// if (
|
// if (
|
||||||
// twoFactorDetails?.enabled &&
|
// twoFactorDetails?.enabled &&
|
||||||
// twoFactorDetails?.secret !== null &&
|
// twoFactorDetails?.secret !== null &&
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,41 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { zodClient } from 'sveltekit-superforms/adapters';
|
import * as Alert from '$components/ui/alert'
|
||||||
import { superForm } from 'sveltekit-superforms/client';
|
import { Button } from '$components/ui/button'
|
||||||
import * as flashModule from 'sveltekit-flash-message/client';
|
import { Input } from '$components/ui/input'
|
||||||
import { AlertCircle } from "lucide-svelte";
|
import { Label } from '$components/ui/label'
|
||||||
import { signInSchema } from '$lib/validations/auth';
|
import * as Card from '$lib/components/ui/card'
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Form from '$lib/components/ui/form'
|
||||||
import * as Form from '$lib/components/ui/form';
|
import { boredState } from '$lib/stores/boredState.js'
|
||||||
import { Label } from '$components/ui/label';
|
import { receive, send } from '$lib/utils/pageCrossfade'
|
||||||
import { Input } from '$components/ui/input';
|
import { signInSchema } from '$lib/validations/auth'
|
||||||
import { Button } from '$components/ui/button';
|
import { AlertCircle } from 'lucide-svelte'
|
||||||
import * as Alert from "$components/ui/alert";
|
import * as flashModule from 'sveltekit-flash-message/client'
|
||||||
import { send, receive } from '$lib/utils/pageCrossfade';
|
import { zodClient } from 'sveltekit-superforms/adapters'
|
||||||
import { boredState } from '$lib/stores/boredState.js';
|
import { superForm } from 'sveltekit-superforms/client'
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props()
|
||||||
|
|
||||||
const superLoginForm = superForm(data.form, {
|
const superLoginForm = superForm(data.form, {
|
||||||
onSubmit: () => boredState.update((n) => ({ ...n, loading: true })),
|
onSubmit: () => boredState.update((n) => ({ ...n, loading: true })),
|
||||||
onResult: () => boredState.update((n) => ({ ...n, loading: false })),
|
onResult: () => boredState.update((n) => ({ ...n, loading: false })),
|
||||||
flashMessage: {
|
flashMessage: {
|
||||||
module: flashModule,
|
module: flashModule,
|
||||||
onError: ({ result, flashMessage }) => {
|
onError: ({ result, flashMessage }) => {
|
||||||
// Error handling for the flash message:
|
// Error handling for the flash message:
|
||||||
// - result is the ActionResult
|
// - result is the ActionResult
|
||||||
// - message is the flash store (not the status message store)
|
// - message is the flash store (not the status message store)
|
||||||
const errorMessage = result.error.message
|
const errorMessage = result.error.message
|
||||||
flashMessage.set({ type: 'error', message: errorMessage });
|
flashMessage.set({ type: 'error', message: errorMessage })
|
||||||
}
|
|
||||||
},
|
},
|
||||||
syncFlashMessage: false,
|
},
|
||||||
taintedMessage: null,
|
syncFlashMessage: false,
|
||||||
// validators: zodClient(signInSchema),
|
taintedMessage: null,
|
||||||
// validationMethod: 'oninput',
|
// validators: zodClient(signInSchema),
|
||||||
delayMs: 0,
|
// validationMethod: 'oninput',
|
||||||
});
|
delayMs: 0,
|
||||||
|
})
|
||||||
|
|
||||||
const { form: loginForm, enhance } = superLoginForm;
|
const { form: loginForm, enhance } = superLoginForm
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -63,6 +63,9 @@
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h1>Sign in</h1>
|
||||||
|
<a href="/login/github">Sign in with GitHub</a>
|
||||||
|
|
||||||
{#snippet usernamePasswordForm()}
|
{#snippet usernamePasswordForm()}
|
||||||
<form method="POST" use:enhance>
|
<form method="POST" use:enhance>
|
||||||
<Form.Field form={superLoginForm} name="username">
|
<Form.Field form={superLoginForm} name="username">
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,19 @@ import { github } from '$lib/server/auth'
|
||||||
import { redirect } from '@sveltejs/kit'
|
import { redirect } from '@sveltejs/kit'
|
||||||
import { generateState } from 'arctic'
|
import { generateState } from 'arctic'
|
||||||
|
|
||||||
export async function GET(event) {
|
import type { RequestEvent } from '@sveltejs/kit'
|
||||||
const state = generateState();
|
|
||||||
|
export async function GET(event: RequestEvent): Promise<Response> {
|
||||||
|
const state = generateState()
|
||||||
const url = await github.createAuthorizationURL(state)
|
const url = await github.createAuthorizationURL(state)
|
||||||
|
|
||||||
event.cookies.set('github_state', state, {
|
event.cookies.set('github_oauth_state', state, {
|
||||||
path: '/',
|
path: '/',
|
||||||
secure: import.meta.env.PROD,
|
secure: import.meta.env.PROD,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
maxAge: 60 * 10,
|
maxAge: 60 * 10,
|
||||||
sameSite: 'lax'
|
sameSite: 'lax',
|
||||||
});
|
})
|
||||||
|
|
||||||
redirect(302, url.toString())
|
redirect(302, url.toString())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue