mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
commit
a8ceafd22c
40 changed files with 3149 additions and 666 deletions
|
|
@ -18,6 +18,12 @@ ADMIN_PASSWORD=
|
|||
|
||||
TWO_FACTOR_TIMEOUT=300000
|
||||
|
||||
# OAuth
|
||||
GITHUB_CLIENT_ID=""
|
||||
GITHUB_CLIENT_SECRET=""
|
||||
GOOGLE_CLIENT_ID=""
|
||||
GOOGLE_CLIENT_SECRET=""
|
||||
|
||||
# Public
|
||||
|
||||
PUBLIC_SITE_NAME='Bored Game'
|
||||
|
|
|
|||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License Copyright (c) 2024 Bradley Shellnut
|
||||
|
||||
Permission is hereby granted,
|
||||
free of charge, to any person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
(including the next paragraph) shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import 'dotenv/config'
|
||||
import env from '$lib/server/api/common/env'
|
||||
import env from './src/lib/server/api/common/env'
|
||||
import { defineConfig } from 'drizzle-kit'
|
||||
|
||||
export default defineConfig({
|
||||
|
|
|
|||
40
package.json
40
package.json
|
|
@ -27,19 +27,20 @@
|
|||
"@faker-js/faker": "^8.4.1",
|
||||
"@melt-ui/pp": "^0.3.2",
|
||||
"@melt-ui/svelte": "^0.83.0",
|
||||
"@playwright/test": "^1.47.0",
|
||||
"@sveltejs/adapter-auto": "^3.2.4",
|
||||
"@sveltejs/enhanced-img": "^0.3.4",
|
||||
"@sveltejs/kit": "^2.5.26",
|
||||
"@playwright/test": "^1.47.2",
|
||||
"@sveltejs/adapter-auto": "^3.2.5",
|
||||
"@sveltejs/enhanced-img": "^0.3.8",
|
||||
"@sveltejs/kit": "^2.5.28",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/node": "^20.16.5",
|
||||
"@types/pg": "^8.11.9",
|
||||
"@types/pg": "^8.11.10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"arctic": "^1.9.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"drizzle-kit": "^0.23.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "2.36.0-next.13",
|
||||
"just-clone": "^6.2.0",
|
||||
|
|
@ -47,13 +48,12 @@
|
|||
"lucia": "3.2.0",
|
||||
"lucide-svelte": "^0.408.0",
|
||||
"nodemailer": "^6.9.15",
|
||||
"postcss": "^8.4.45",
|
||||
"postcss": "^8.4.47",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-load-config": "^5.1.0",
|
||||
"postcss-preset-env": "^9.6.0",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"sass": "^1.78.0",
|
||||
"satori": "^0.10.14",
|
||||
"satori-html": "^0.3.2",
|
||||
"svelte": "5.0.0-next.175",
|
||||
|
|
@ -64,13 +64,13 @@
|
|||
"svelte-sequential-preprocessor": "^2.0.1",
|
||||
"sveltekit-flash-message": "^2.4.4",
|
||||
"sveltekit-rate-limiter": "^0.5.2",
|
||||
"sveltekit-superforms": "^2.17.0",
|
||||
"tailwindcss": "^3.4.11",
|
||||
"sveltekit-superforms": "^2.19.0",
|
||||
"tailwindcss": "^3.4.12",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.7.0",
|
||||
"tsx": "^4.19.1",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.4",
|
||||
"vite": "^5.4.7",
|
||||
"vitest": "^1.6.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
|
|
@ -88,14 +88,13 @@
|
|||
"@neondatabase/serverless": "^0.9.5",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@sveltejs/adapter-node": "^5.2.2",
|
||||
"@sveltejs/adapter-vercel": "^5.4.3",
|
||||
"@sveltejs/adapter-node": "^5.2.4",
|
||||
"@sveltejs/adapter-vercel": "^5.4.4",
|
||||
"@types/feather-icons": "^4.29.4",
|
||||
"@vercel/og": "^0.5.20",
|
||||
"arctic": "^1.9.2",
|
||||
"bits-ui": "^0.21.13",
|
||||
"bits-ui": "^0.21.15",
|
||||
"boardgamegeekclient": "^1.9.1",
|
||||
"bullmq": "^5.13.0",
|
||||
"bullmq": "^5.13.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cookie": "^0.6.0",
|
||||
|
|
@ -106,7 +105,7 @@
|
|||
"feather-icons": "^4.29.2",
|
||||
"formsnap": "^1.0.1",
|
||||
"handlebars": "^4.7.8",
|
||||
"hono": "^4.6.1",
|
||||
"hono": "^4.6.2",
|
||||
"hono-rate-limiter": "^0.4.0",
|
||||
"html-entities": "^2.5.2",
|
||||
"iconify-icon": "^2.1.0",
|
||||
|
|
@ -116,7 +115,7 @@
|
|||
"loader": "^2.1.1",
|
||||
"open-props": "^1.7.6",
|
||||
"oslo": "^1.2.1",
|
||||
"pg": "^8.12.0",
|
||||
"pg": "^8.13.0",
|
||||
"postgres": "^3.4.4",
|
||||
"qrcode": "^1.5.4",
|
||||
"radix-svelte": "^0.9.0",
|
||||
|
|
@ -129,5 +128,6 @@
|
|||
"tailwindcss-animate": "^1.0.7",
|
||||
"tsyringe": "^4.8.0",
|
||||
"zod-to-json-schema": "^3.23.3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
1042
pnpm-lock.yaml
1042
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -17,6 +17,10 @@ const EnvSchema = z.object({
|
|||
DATABASE_DB: z.string(),
|
||||
DB_MIGRATING: stringBoolean,
|
||||
DB_SEEDING: stringBoolean,
|
||||
GITHUB_CLIENT_ID: z.string(),
|
||||
GITHUB_CLIENT_SECRET: z.string(),
|
||||
GOOGLE_CLIENT_ID: z.string(),
|
||||
GOOGLE_CLIENT_SECRET: z.string(),
|
||||
NODE_ENV: z.string().default('development'),
|
||||
ORIGIN: z.string(),
|
||||
PUBLIC_SITE_NAME: z.string(),
|
||||
|
|
|
|||
11
src/lib/server/api/common/types/oauth.ts
Normal file
11
src/lib/server/api/common/types/oauth.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export type OAuthUser = {
|
||||
sub: string;
|
||||
given_name?: string;
|
||||
family_name?: string;
|
||||
picture?: string;
|
||||
username: string;
|
||||
email?: string;
|
||||
email_verified?: boolean;
|
||||
}
|
||||
|
||||
export type OAuthProviders = 'github' | 'google' | 'apple'
|
||||
|
|
@ -18,6 +18,11 @@ export class CollectionController extends Controller {
|
|||
console.log('collections service', collections)
|
||||
return c.json({ collections })
|
||||
})
|
||||
.get('/count', requireAuth, async (c) => {
|
||||
const user = c.var.user
|
||||
const collections = await this.collectionsService.findAllByUserIdWithDetails(user.id)
|
||||
return c.json({ collections })
|
||||
})
|
||||
.get('/:cuid', requireAuth, async (c) => {
|
||||
const cuid = c.req.param('cuid')
|
||||
const collection = await this.collectionsService.findOneByCuid(cuid)
|
||||
|
|
|
|||
150
src/lib/server/api/controllers/oauth.controller.ts
Normal file
150
src/lib/server/api/controllers/oauth.controller.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import 'reflect-metadata'
|
||||
import { Controller } from '$lib/server/api/common/types/controller'
|
||||
import { LuciaService } from '$lib/server/api/services/lucia.service'
|
||||
import { OAuthService } from '$lib/server/api/services/oauth.service'
|
||||
import { github, google } from '$lib/server/auth'
|
||||
import { OAuth2RequestError } from 'arctic'
|
||||
import { getCookie, setCookie } from 'hono/cookie'
|
||||
import { TimeSpan } from 'oslo'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import type {OAuthUser} from "$lib/server/api/common/types/oauth";
|
||||
|
||||
@injectable()
|
||||
export class OAuthController extends Controller {
|
||||
constructor(
|
||||
@inject(LuciaService) private luciaService: LuciaService,
|
||||
@inject(OAuthService) private oauthService: OAuthService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
routes() {
|
||||
return this.controller
|
||||
.get('/github', async (c) => {
|
||||
try {
|
||||
const code = c.req.query('code')?.toString() ?? null
|
||||
const state = c.req.query('state')?.toString() ?? null
|
||||
const storedState = getCookie(c).github_oauth_state ?? null
|
||||
|
||||
if (!code || !state || !storedState || state !== storedState) {
|
||||
return c.body(null, 400)
|
||||
}
|
||||
|
||||
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 oAuthUser: OAuthUser = {
|
||||
sub: `${githubUser.id}`,
|
||||
username: githubUser.login,
|
||||
email: undefined
|
||||
}
|
||||
|
||||
const userId = await this.oauthService.handleOAuthUser(oAuthUser, '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)
|
||||
}
|
||||
})
|
||||
.get('/google', async (c) => {
|
||||
try {
|
||||
const code = c.req.query('code')?.toString() ?? null
|
||||
const state = c.req.query('state')?.toString() ?? null
|
||||
const storedState = getCookie(c).google_oauth_state ?? null
|
||||
const storedCodeVerifier = getCookie(c).google_oauth_code_verifier ?? null
|
||||
|
||||
if (!code || !storedState || !storedCodeVerifier || state !== storedState) {
|
||||
return c.body(null, 400)
|
||||
}
|
||||
|
||||
const tokens = await google.validateAuthorizationCode(code, storedCodeVerifier)
|
||||
const googleUserResponse = await fetch("https://openidconnect.googleapis.com/v1/userinfo", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.accessToken}`,
|
||||
},
|
||||
})
|
||||
const googleUser: GoogleUser = await googleUserResponse.json()
|
||||
|
||||
const oAuthUser: OAuthUser = {
|
||||
sub: googleUser.sub,
|
||||
given_name: googleUser.given_name,
|
||||
family_name: googleUser.family_name,
|
||||
picture: googleUser.picture,
|
||||
username: googleUser.email,
|
||||
email: googleUser.email,
|
||||
email_verified: googleUser.email_verified,
|
||||
}
|
||||
|
||||
const userId = await this.oauthService.handleOAuthUser(oAuthUser, 'google')
|
||||
|
||||
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 {
|
||||
id: number
|
||||
login: string
|
||||
}
|
||||
|
||||
interface GoogleUser {
|
||||
sub: string
|
||||
name: string
|
||||
given_name: string
|
||||
family_name: string
|
||||
picture: string
|
||||
email: string
|
||||
email_verified: boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "users" ADD COLUMN "email_verified" boolean DEFAULT false;--> statement-breakpoint
|
||||
ALTER TABLE "users" ADD COLUMN "picture" text;
|
||||
1876
src/lib/server/api/databases/migrations/meta/0001_snapshot.json
Normal file
1876
src/lib/server/api/databases/migrations/meta/0001_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,13 @@
|
|||
"when": 1725489682980,
|
||||
"tag": "0000_volatile_warhawk",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1726877846811,
|
||||
"tag": "0001_pink_the_enforcers",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { type InferSelectModel, relations } from 'drizzle-orm'
|
|||
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
|
||||
import { timestamps } from '../../common/utils/table'
|
||||
import { usersTable } from './users.table'
|
||||
import { collection_items } from './collectionItems.table'
|
||||
|
||||
export const collections = pgTable('collections', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
|
@ -16,11 +17,12 @@ export const collections = pgTable('collections', {
|
|||
...timestamps,
|
||||
})
|
||||
|
||||
export const collection_relations = relations(collections, ({ one }) => ({
|
||||
export const collection_relations = relations(collections, ({ one, many }) => ({
|
||||
user: one(usersTable, {
|
||||
fields: [collections.user_id],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
collection_items: many(collection_items),
|
||||
}))
|
||||
|
||||
export type Collections = InferSelectModel<typeof collections>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ export const usersTable = pgTable('users', {
|
|||
last_name: text('last_name'),
|
||||
verified: boolean('verified').default(false),
|
||||
receive_email: boolean('receive_email').default(false),
|
||||
email_verified: boolean('email_verified').default(false),
|
||||
picture: text('picture'),
|
||||
mfa_enabled: boolean('mfa_enabled').notNull().default(false),
|
||||
theme: text('theme').default('system'),
|
||||
...timestamps,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'reflect-metadata'
|
||||
import { CollectionController } from '$lib/server/api/controllers/collection.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 { UserController } from '$lib/server/api/controllers/user.controller'
|
||||
import { WishlistController } from '$lib/server/api/controllers/wishlist.controller'
|
||||
|
|
@ -44,6 +45,7 @@ const routes = app
|
|||
.route('/me', container.resolve(IamController).routes())
|
||||
.route('/user', container.resolve(UserController).routes())
|
||||
.route('/login', container.resolve(LoginController).routes())
|
||||
.route('/oauth', container.resolve(OAuthController).routes())
|
||||
.route('/signup', container.resolve(SignupController).routes())
|
||||
.route('/wishlists', container.resolve(WishlistController).routes())
|
||||
.route('/collections', container.resolve(CollectionController).routes())
|
||||
|
|
|
|||
|
|
@ -51,6 +51,23 @@ export class CollectionsRepository {
|
|||
})
|
||||
}
|
||||
|
||||
async findAllByUserIdWithDetails(userId: string, db = this.drizzle.db) {
|
||||
return db.query.collections.findMany({
|
||||
where: eq(collections.user_id, userId),
|
||||
columns: {
|
||||
cuid: true,
|
||||
name: true,
|
||||
},
|
||||
with: {
|
||||
collection_items: {
|
||||
columns: {
|
||||
cuid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async create(data: CreateCollection, db = this.drizzle.db) {
|
||||
return db.insert(collections).values(data).returning().then(takeFirstOrThrow)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export class CredentialsRepository {
|
|||
}
|
||||
|
||||
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')
|
||||
return credentials
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { type InferInsertModel, and, eq } from 'drizzle-orm'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { takeFirstOrThrow } from '../common/utils/repository'
|
||||
import { federatedIdentityTable } from '../databases/tables'
|
||||
import { DrizzleService } from '../services/drizzle.service'
|
||||
|
||||
export type CreateFederatedIdentity = InferInsertModel<typeof federatedIdentityTable>
|
||||
|
||||
@injectable()
|
||||
export class FederatedIdentityRepository {
|
||||
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {}
|
||||
|
||||
async findOneByUserIdAndProvider(userId: string, provider: string) {
|
||||
return this.drizzle.db.query.federatedIdentityTable.findFirst({
|
||||
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) {
|
||||
return db.insert(federatedIdentityTable).values(data).returning().then(takeFirstOrThrow)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,29 +28,29 @@ export class RolesRepository {
|
|||
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {}
|
||||
|
||||
async findOneById(id: string, db = this.drizzle.db) {
|
||||
return db.query.roles.findFirst({
|
||||
return db.query.rolesTable.findFirst({
|
||||
where: eq(rolesTable.id, id),
|
||||
})
|
||||
}
|
||||
|
||||
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')
|
||||
return role
|
||||
}
|
||||
|
||||
async findAll(db = this.drizzle.db) {
|
||||
return db.query.roles.findMany()
|
||||
return db.query.rolesTable.findMany()
|
||||
}
|
||||
|
||||
async findOneByName(name: string, db = this.drizzle.db) {
|
||||
return db.query.roles.findFirst({
|
||||
return db.query.rolesTable.findFirst({
|
||||
where: eq(rolesTable.name, name),
|
||||
})
|
||||
}
|
||||
|
||||
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')
|
||||
return role
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ export class UserRolesRepository {
|
|||
})
|
||||
}
|
||||
|
||||
async findOneByIdOrThrow(id: string) {
|
||||
const userRole = await this.findOneById(id)
|
||||
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
|
||||
const userRole = await this.findOneById(id, db)
|
||||
if (!userRole) throw Error('User not found')
|
||||
return userRole
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,50 @@
|
|||
import { inject, injectable } from "tsyringe";
|
||||
import { generateRandomAnimalName } from "$lib/utils/randomDataUtil";
|
||||
import { CollectionsRepository } from "../repositories/collections.repository";
|
||||
import type { db } from '$lib/server/api/packages/drizzle'
|
||||
import { generateRandomAnimalName } from '$lib/utils/randomDataUtil'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { CollectionsRepository } from '../repositories/collections.repository'
|
||||
|
||||
@injectable()
|
||||
export class CollectionsService {
|
||||
constructor(
|
||||
@inject(CollectionsRepository) private readonly collectionsRepository: CollectionsRepository
|
||||
) { }
|
||||
constructor(@inject(CollectionsRepository) private readonly collectionsRepository: CollectionsRepository) {}
|
||||
|
||||
async findOneByUserId(userId: string) {
|
||||
return this.collectionsRepository.findOneByUserId(userId);
|
||||
return this.collectionsRepository.findOneByUserId(userId)
|
||||
}
|
||||
|
||||
async findAllByUserId(userId: string) {
|
||||
return this.collectionsRepository.findAllByUserId(userId);
|
||||
return this.collectionsRepository.findAllByUserId(userId)
|
||||
}
|
||||
|
||||
async findAllByUserIdWithDetails(userId: string) {
|
||||
return this.collectionsRepository.findAllByUserIdWithDetails(userId)
|
||||
}
|
||||
|
||||
async findOneById(id: string) {
|
||||
return this.collectionsRepository.findOneById(id);
|
||||
return this.collectionsRepository.findOneById(id)
|
||||
}
|
||||
|
||||
async findOneByCuid(cuid: string) {
|
||||
return this.collectionsRepository.findOneByCuid(cuid);
|
||||
return this.collectionsRepository.findOneByCuid(cuid)
|
||||
}
|
||||
|
||||
async createEmptyNoName(userId: string) {
|
||||
return this.createEmpty(userId, null);
|
||||
async createEmptyNoName(userId: string, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
|
||||
return this.createEmpty(userId, null, trx)
|
||||
}
|
||||
|
||||
async createEmpty(userId: string, name: string | null) {
|
||||
return this.collectionsRepository.create({
|
||||
user_id: userId,
|
||||
name: name ?? generateRandomAnimalName(),
|
||||
});
|
||||
async createEmpty(userId: string, name: string | null, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
|
||||
if (!trx) {
|
||||
return this.collectionsRepository.create({
|
||||
user_id: userId,
|
||||
name: name ?? generateRandomAnimalName(),
|
||||
})
|
||||
}
|
||||
|
||||
return this.collectionsRepository.create(
|
||||
{
|
||||
user_id: userId,
|
||||
name: name ?? generateRandomAnimalName(),
|
||||
},
|
||||
trx,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
src/lib/server/api/services/oauth.service.ts
Normal file
27
src/lib/server/api/services/oauth.service.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { inject, injectable } from 'tsyringe'
|
||||
import { FederatedIdentityRepository } from '../repositories/federated_identity.repository'
|
||||
import { UsersService } from './users.service'
|
||||
import type {OAuthUser, OAuthProviders} from "$lib/server/api/common/types/oauth";
|
||||
|
||||
@injectable()
|
||||
export class OAuthService {
|
||||
constructor(
|
||||
@inject(FederatedIdentityRepository) private readonly federatedIdentityRepository: FederatedIdentityRepository,
|
||||
@inject(UsersService) private readonly usersService: UsersService,
|
||||
) {}
|
||||
|
||||
async handleOAuthUser(oAuthUser: OAuthUser, oauthProvider: OAuthProviders) {
|
||||
const federatedUser = await this.federatedIdentityRepository.findOneByFederatedUserIdAndProvider(oAuthUser.sub, oauthProvider)
|
||||
|
||||
if (federatedUser) {
|
||||
return federatedUser.user_id
|
||||
}
|
||||
|
||||
const user = await this.usersService.createOAuthUser(oAuthUser, oauthProvider)
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Failed to create user')
|
||||
}
|
||||
return user.id
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +1,51 @@
|
|||
import {inject, injectable} from "tsyringe";
|
||||
import {type CreateUserRole, UserRolesRepository} from "$lib/server/api/repositories/user_roles.repository";
|
||||
import {RolesService} from "$lib/server/api/services/roles.service";
|
||||
import type { db } from '$lib/server/api/packages/drizzle'
|
||||
import { type CreateUserRole, UserRolesRepository } from '$lib/server/api/repositories/user_roles.repository'
|
||||
import { RolesService } from '$lib/server/api/services/roles.service'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
|
||||
@injectable()
|
||||
export class UserRolesService {
|
||||
constructor(
|
||||
@inject(UserRolesRepository) private readonly userRolesRepository: UserRolesRepository,
|
||||
@inject(RolesService) private readonly rolesService: RolesService
|
||||
) { }
|
||||
@inject(UserRolesRepository) private readonly userRolesRepository: UserRolesRepository,
|
||||
@inject(RolesService) private readonly rolesService: RolesService,
|
||||
) {}
|
||||
|
||||
async findOneById(id: string) {
|
||||
return this.userRolesRepository.findOneById(id);
|
||||
return this.userRolesRepository.findOneById(id)
|
||||
}
|
||||
|
||||
async findAllByUserId(userId: string) {
|
||||
return this.userRolesRepository.findAllByUserId(userId);
|
||||
return this.userRolesRepository.findAllByUserId(userId)
|
||||
}
|
||||
|
||||
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
|
||||
const role = await this.rolesService.findOneByNameOrThrow(roleName);
|
||||
const role = await this.rolesService.findOneByNameOrThrow(roleName)
|
||||
|
||||
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
|
||||
return this.userRolesRepository.create({
|
||||
user_id: userId,
|
||||
role_id: role.id,
|
||||
primary,
|
||||
});
|
||||
return this.userRolesRepository.create(
|
||||
{
|
||||
user_id: userId,
|
||||
role_id: role.id,
|
||||
primary,
|
||||
},
|
||||
trx,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,28 @@
|
|||
import type { SignupUsernameEmailDto } from '$lib/server/api/dtos/signup-username-email.dto'
|
||||
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 { UserRolesService } from '$lib/server/api/services/user_roles.service'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { CredentialsType } from '../databases/tables'
|
||||
import {CredentialsType, RoleName} from '../databases/tables'
|
||||
import { type UpdateUser, UsersRepository } from '../repositories/users.repository'
|
||||
import { CollectionsService } from './collections.service'
|
||||
import { DrizzleService } from './drizzle.service'
|
||||
import { WishlistsService } from './wishlists.service'
|
||||
import type {OAuthUser} from "$lib/server/api/common/types/oauth";
|
||||
|
||||
@injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@inject(CollectionsService) private readonly collectionsService: CollectionsService,
|
||||
@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(UsersRepository) private readonly usersRepository: UsersRepository,
|
||||
@inject(UserRolesService) private readonly userRolesService: UserRolesService,
|
||||
@inject(WishlistsRepository) private readonly wishlistsRepository: WishlistsRepository,
|
||||
@inject(WishlistsService) private readonly wishlistsService: WishlistsService,
|
||||
) {}
|
||||
|
||||
|
|
@ -23,34 +30,76 @@ export class UsersService {
|
|||
const { firstName, lastName, email, username, password } = data
|
||||
|
||||
const hashedPassword = await this.tokenService.createHashedToken(password)
|
||||
const user = await this.usersRepository.create({
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
email,
|
||||
username,
|
||||
return await this.drizzleService.db.transaction(async (trx) => {
|
||||
const createdUser = await this.usersRepository.create(
|
||||
{
|
||||
first_name: firstName,
|
||||
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, RoleName.USER, true, trx)
|
||||
|
||||
await this.wishlistsService.createEmptyNoName(createdUser.id, trx)
|
||||
await this.collectionsService.createEmptyNoName(createdUser.id, trx)
|
||||
})
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
async createOAuthUser(oAuthUser: OAuthUser, oauthProvider: string) {
|
||||
return await this.drizzleService.db.transaction(async (trx) => {
|
||||
const createdUser = await this.usersRepository.create(
|
||||
{
|
||||
username: oAuthUser.username || oAuthUser.username,
|
||||
email: oAuthUser.email || null,
|
||||
first_name: oAuthUser.given_name || null,
|
||||
last_name: oAuthUser.family_name || null,
|
||||
picture: oAuthUser.picture || null,
|
||||
email_verified: oAuthUser.email_verified || false,
|
||||
},
|
||||
trx,
|
||||
)
|
||||
|
||||
const credentials = await this.credentialsRepository.create({
|
||||
user_id: user.id,
|
||||
type: CredentialsType.PASSWORD,
|
||||
secret_data: hashedPassword,
|
||||
if (!createdUser) {
|
||||
return null
|
||||
}
|
||||
|
||||
await this.federatedIdentityRepository.create(
|
||||
{
|
||||
identity_provider: oauthProvider,
|
||||
user_id: createdUser.id,
|
||||
federated_user_id: oAuthUser.sub,
|
||||
federated_username: oAuthUser.email || oAuthUser.username,
|
||||
},
|
||||
trx,
|
||||
)
|
||||
|
||||
await this.userRolesService.addRoleToUser(createdUser.id, RoleName.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) {
|
||||
|
|
|
|||
|
|
@ -1,34 +1,41 @@
|
|||
import { inject, injectable } from "tsyringe";
|
||||
import { WishlistsRepository } from "../repositories/wishlists.repository";
|
||||
import { generateRandomAnimalName } from "$lib/utils/randomDataUtil";
|
||||
import type { db } from '$lib/server/api/packages/drizzle'
|
||||
import { generateRandomAnimalName } from '$lib/utils/randomDataUtil'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { WishlistsRepository } from '../repositories/wishlists.repository'
|
||||
|
||||
@injectable()
|
||||
export class WishlistsService {
|
||||
|
||||
constructor(
|
||||
@inject(WishlistsRepository) private readonly wishlistsRepository: WishlistsRepository
|
||||
) { }
|
||||
constructor(@inject(WishlistsRepository) private readonly wishlistsRepository: WishlistsRepository) {}
|
||||
|
||||
async findAllByUserId(userId: string) {
|
||||
return this.wishlistsRepository.findAllByUserId(userId);
|
||||
return this.wishlistsRepository.findAllByUserId(userId)
|
||||
}
|
||||
|
||||
async findOneById(id: string) {
|
||||
return this.wishlistsRepository.findOneById(id);
|
||||
return this.wishlistsRepository.findOneById(id)
|
||||
}
|
||||
|
||||
async findOneByCuid(cuid: string) {
|
||||
return this.wishlistsRepository.findOneByCuid(cuid);
|
||||
return this.wishlistsRepository.findOneByCuid(cuid)
|
||||
}
|
||||
|
||||
async createEmptyNoName(userId: string) {
|
||||
return this.createEmpty(userId, null);
|
||||
async createEmptyNoName(userId: string, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
|
||||
return this.createEmpty(userId, null, trx)
|
||||
}
|
||||
|
||||
async createEmpty(userId: string, name: string | null) {
|
||||
return this.wishlistsRepository.create({
|
||||
user_id: userId,
|
||||
name: name ?? generateRandomAnimalName(),
|
||||
});
|
||||
async createEmpty(userId: string, name: string | null, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
|
||||
if (!trx) {
|
||||
return this.wishlistsRepository.create({
|
||||
user_id: userId,
|
||||
name: name ?? generateRandomAnimalName(),
|
||||
})
|
||||
}
|
||||
return this.wishlistsRepository.create(
|
||||
{
|
||||
user_id: userId,
|
||||
name: name ?? generateRandomAnimalName(),
|
||||
},
|
||||
trx,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
src/lib/server/auth.ts
Normal file
6
src/lib/server/auth.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import env from "$lib/server/api/common/env";
|
||||
import { GitHub, Google } from "arctic";
|
||||
|
||||
export const github = new GitHub(env.GITHUB_CLIENT_ID, env.GITHUB_CLIENT_SECRET);
|
||||
|
||||
export const google = new Google(env.GOOGLE_CLIENT_ID, env.GOOGLE_CLIENT_SECRET, `${env.ORIGIN}/auth/callback/google`);
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
import { notSignedInMessage } from '$lib/flashMessages'
|
||||
import { collection_items, collections, gamesTable } from '$lib/server/api/databases/tables'
|
||||
import { db } from '$lib/server/api/packages/drizzle'
|
||||
import { userNotAuthenticated } from '$lib/server/auth-utils'
|
||||
import { modifyListGameSchema } from '$lib/validations/zod-schemas'
|
||||
import { type Actions, error, fail } from '@sveltejs/kit'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { redirect } from 'sveltekit-flash-message/server'
|
||||
import { zod } from 'sveltekit-superforms/adapters'
|
||||
import { superValidate } from 'sveltekit-superforms/server'
|
||||
import { collection_items, collections, gamesTable } from '../../../../lib/server/api/databases/tables'
|
||||
|
||||
export async function load(event) {
|
||||
const { locals } = event
|
||||
|
|
@ -18,23 +17,10 @@ export async function load(event) {
|
|||
}
|
||||
|
||||
try {
|
||||
const userCollections = await db.query.collections.findMany({
|
||||
columns: {
|
||||
cuid: true,
|
||||
name: true,
|
||||
created_at: true,
|
||||
},
|
||||
where: eq(collections.user_id, authedUser.id),
|
||||
})
|
||||
console.log('collections', userCollections)
|
||||
|
||||
if (userCollections?.length === 0) {
|
||||
console.log('Collection was not found')
|
||||
return fail(404, {})
|
||||
}
|
||||
const { data, error } = await locals.api.collections.$get().then(locals.parseApiResponse)
|
||||
|
||||
return {
|
||||
collections: userCollections,
|
||||
collections: data?.collections || [],
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
|
|
|||
|
|
@ -1,24 +1,34 @@
|
|||
<script lang="ts">
|
||||
const { data } = $props();
|
||||
let collections = data?.collections || [];
|
||||
import * as Card from '$components/ui/card'
|
||||
const { data } = $props()
|
||||
let collections = data?.collections || []
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Your Collections | Bored Game</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<h1>Your Collections</h1>
|
||||
|
||||
<div class="collections">
|
||||
<div class="collection-list">
|
||||
{#if collections.length === 0}
|
||||
<h2>You have no collections</h2>
|
||||
{:else}
|
||||
{#each collections as collection}
|
||||
<div class="collection grid gap-0.5">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{collection.name}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<p>Number of items:</p>
|
||||
<p>Created at: {new Date(collection.createdAt).toLocaleString()}</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<!-- <div class="collection grid gap-0.5">
|
||||
<h2><a href="/collections/{collection.cuid}">{collection.name}</a></h2>
|
||||
<h3>Created at: {new Date(collection.created_at).toLocaleString()}</h3>
|
||||
</div>
|
||||
<h3>Created at: {new Date(collection.createdAt).toLocaleString()}</h3>
|
||||
</div> -->
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -30,10 +40,6 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.collections {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.collection-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(200px, 1fr));
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ console.log('items', items)
|
|||
<div class="games">
|
||||
<div class="games-list">
|
||||
{#if items.length === 0}
|
||||
<h2>No gamesTable in your collection</h2>
|
||||
<h2>No games in your collection</h2>
|
||||
{:else}
|
||||
{#each items as game (game.game_id)}
|
||||
<Game {game} />
|
||||
|
|
|
|||
|
|
@ -18,14 +18,6 @@ export const load: PageServerLoad = async (event) => {
|
|||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
|
||||
console.log('authedUser', authedUser)
|
||||
// if (userNotAuthenticated(user, session)) {
|
||||
// redirect(302, '/login', notSignedInMessage, event);
|
||||
// }
|
||||
// const dbUser = await db.query.usersTable.findFirst({
|
||||
// where: eq(usersTable.id, user!.id!),
|
||||
// });
|
||||
|
||||
const profileForm = await superValidate(zod(updateProfileSchema), {
|
||||
defaults: {
|
||||
firstName: authedUser?.firstName ?? '',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<h1>Privacy Policy</h1>
|
||||
<h2>Last Updated: September 13th, 2023</h2>
|
||||
<h2>Last Updated: September 19th, 2024</h2>
|
||||
|
||||
At Bored Game, we respect your privacy and are committed to protecting your personal information.
|
||||
We collect only the personal information that is necessary for us to provide our services to you.
|
||||
|
|
|
|||
26
src/routes/(auth)/auth/callback/github/+server.ts
Normal file
26
src/routes/(auth)/auth/callback/github/+server.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { StatusCodes } from '$lib/constants/status-codes'
|
||||
import type { RequestEvent } from '@sveltejs/kit'
|
||||
import { redirect } from 'sveltekit-flash-message/server'
|
||||
|
||||
export async function GET(event: RequestEvent): Promise<Response> {
|
||||
const { locals, url } = event
|
||||
const code = url.searchParams.get('code')
|
||||
const state = url.searchParams.get('state')
|
||||
console.log('code', code, 'state', 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, '/')
|
||||
}
|
||||
26
src/routes/(auth)/auth/callback/google/+server.ts
Normal file
26
src/routes/(auth)/auth/callback/google/+server.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { StatusCodes } from '$lib/constants/status-codes'
|
||||
import type { RequestEvent } from '@sveltejs/kit'
|
||||
import { redirect } from 'sveltekit-flash-message/server'
|
||||
|
||||
export async function GET(event: RequestEvent): Promise<Response> {
|
||||
const { locals, url } = event
|
||||
const code = url.searchParams.get('code')
|
||||
const state = url.searchParams.get('state')
|
||||
console.log('code', code, 'state', state)
|
||||
|
||||
const { data, error } = await locals.api.oauth.google.$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, '/')
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { StatusCodes } from '$lib/constants/status-codes'
|
||||
import { signinUsernameDto } from '$lib/dtos/signin-username.dto'
|
||||
import { type Actions, fail } from '@sveltejs/kit'
|
||||
import { redirect } from 'sveltekit-flash-message/server'
|
||||
|
|
@ -136,6 +137,8 @@ export const actions: Actions = {
|
|||
form.data.username = ''
|
||||
form.data.password = ''
|
||||
|
||||
redirect(StatusCodes.TEMPORARY_REDIRECT, '/')
|
||||
|
||||
// if (
|
||||
// twoFactorDetails?.enabled &&
|
||||
// twoFactorDetails?.secret !== null &&
|
||||
|
|
|
|||
|
|
@ -1,41 +1,41 @@
|
|||
<script lang="ts">
|
||||
import { zodClient } from 'sveltekit-superforms/adapters';
|
||||
import { superForm } from 'sveltekit-superforms/client';
|
||||
import * as flashModule from 'sveltekit-flash-message/client';
|
||||
import { AlertCircle } from "lucide-svelte";
|
||||
import { signInSchema } from '$lib/validations/auth';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import * as Form from '$lib/components/ui/form';
|
||||
import { Label } from '$components/ui/label';
|
||||
import { Input } from '$components/ui/input';
|
||||
import { Button } from '$components/ui/button';
|
||||
import * as Alert from "$components/ui/alert";
|
||||
import { send, receive } from '$lib/utils/pageCrossfade';
|
||||
import { boredState } from '$lib/stores/boredState.js';
|
||||
import * as Alert from '$components/ui/alert'
|
||||
import { Button } from '$components/ui/button'
|
||||
import { Input } from '$components/ui/input'
|
||||
import { Label } from '$components/ui/label'
|
||||
import * as Card from '$lib/components/ui/card'
|
||||
import * as Form from '$lib/components/ui/form'
|
||||
import { boredState } from '$lib/stores/boredState.js'
|
||||
import { receive, send } from '$lib/utils/pageCrossfade'
|
||||
import { signInSchema } from '$lib/validations/auth'
|
||||
import { AlertCircle } from 'lucide-svelte'
|
||||
import * as flashModule from 'sveltekit-flash-message/client'
|
||||
import { zodClient } from 'sveltekit-superforms/adapters'
|
||||
import { superForm } from 'sveltekit-superforms/client'
|
||||
|
||||
let { data } = $props();
|
||||
let { data } = $props()
|
||||
|
||||
const superLoginForm = superForm(data.form, {
|
||||
onSubmit: () => boredState.update((n) => ({ ...n, loading: true })),
|
||||
onResult: () => boredState.update((n) => ({ ...n, loading: false })),
|
||||
flashMessage: {
|
||||
module: flashModule,
|
||||
onError: ({ result, flashMessage }) => {
|
||||
// Error handling for the flash message:
|
||||
// - result is the ActionResult
|
||||
// - message is the flash store (not the status message store)
|
||||
const errorMessage = result.error.message
|
||||
flashMessage.set({ type: 'error', message: errorMessage });
|
||||
}
|
||||
const superLoginForm = superForm(data.form, {
|
||||
onSubmit: () => boredState.update((n) => ({ ...n, loading: true })),
|
||||
onResult: () => boredState.update((n) => ({ ...n, loading: false })),
|
||||
flashMessage: {
|
||||
module: flashModule,
|
||||
onError: ({ result, flashMessage }) => {
|
||||
// Error handling for the flash message:
|
||||
// - result is the ActionResult
|
||||
// - message is the flash store (not the status message store)
|
||||
const errorMessage = result.error.message
|
||||
flashMessage.set({ type: 'error', message: errorMessage })
|
||||
},
|
||||
syncFlashMessage: false,
|
||||
taintedMessage: null,
|
||||
// validators: zodClient(signInSchema),
|
||||
// validationMethod: 'oninput',
|
||||
delayMs: 0,
|
||||
});
|
||||
},
|
||||
syncFlashMessage: false,
|
||||
taintedMessage: null,
|
||||
// validators: zodClient(signInSchema),
|
||||
// validationMethod: 'oninput',
|
||||
delayMs: 0,
|
||||
})
|
||||
|
||||
const { form: loginForm, enhance } = superLoginForm;
|
||||
const { form: loginForm, enhance } = superLoginForm
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -47,8 +47,10 @@
|
|||
<Card.Header>
|
||||
<Card.Title class="text-2xl">Log into your account</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<Card.Content class="grid gap-4">
|
||||
{@render usernamePasswordForm()}
|
||||
<span class="text-center text-sm text-muted-foreground">or sign in with</span>
|
||||
{@render oAuthButtons()}
|
||||
<p class="px-8 py-4 text-center text-sm text-muted-foreground">
|
||||
By clicking continue, you agree to our
|
||||
<a href="/terms" class="underline underline-offset-4 hover:text-primary">
|
||||
|
|
@ -86,5 +88,17 @@
|
|||
</form>
|
||||
{/snippet}
|
||||
|
||||
{#snippet oAuthButtons()}
|
||||
<div class="grid gap-4">
|
||||
<Button href="/login/google" variant="outline" class="w-full flex items-center gap-2"><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google</title><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg> Google</Button>
|
||||
<Button href="/login/apple" variant="outline" class="w-full flex items-center gap-2"><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Apple</title><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/></svg> Apple</Button>
|
||||
<Button href="/login/github" variant="outline" class="w-full flex items-center gap-2"><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg> GitHub</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<style lang="postcss">
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
</style>
|
||||
20
src/routes/(auth)/login/apple/+server.ts
Normal file
20
src/routes/(auth)/login/apple/+server.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { github } from '$lib/server/auth'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
import { generateState } from 'arctic'
|
||||
|
||||
import type { RequestEvent } from '@sveltejs/kit'
|
||||
|
||||
export async function GET(event: RequestEvent): Promise<Response> {
|
||||
const state = generateState()
|
||||
const url = await github.createAuthorizationURL(state)
|
||||
|
||||
event.cookies.set('github_oauth_state', state, {
|
||||
path: '/',
|
||||
secure: import.meta.env.PROD,
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 10,
|
||||
sameSite: 'lax',
|
||||
})
|
||||
|
||||
redirect(302, url.toString())
|
||||
}
|
||||
20
src/routes/(auth)/login/github/+server.ts
Normal file
20
src/routes/(auth)/login/github/+server.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { github } from '$lib/server/auth'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
import { generateState } from 'arctic'
|
||||
|
||||
import type { RequestEvent } from '@sveltejs/kit'
|
||||
|
||||
export async function GET(event: RequestEvent): Promise<Response> {
|
||||
const state = generateState()
|
||||
const url = await github.createAuthorizationURL(state)
|
||||
|
||||
event.cookies.set('github_oauth_state', state, {
|
||||
path: '/',
|
||||
secure: import.meta.env.PROD,
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 10,
|
||||
sameSite: 'lax',
|
||||
})
|
||||
|
||||
redirect(302, url.toString())
|
||||
}
|
||||
33
src/routes/(auth)/login/google/+server.ts
Normal file
33
src/routes/(auth)/login/google/+server.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { google } from '$lib/server/auth'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
import { generateCodeVerifier, generateState } from 'arctic'
|
||||
|
||||
import type { RequestEvent } from '@sveltejs/kit'
|
||||
|
||||
// Google Login
|
||||
export async function GET(event: RequestEvent): Promise<Response> {
|
||||
const state = generateState()
|
||||
const codeVerifier = generateCodeVerifier();
|
||||
|
||||
const url = await google.createAuthorizationURL(state, codeVerifier, {
|
||||
scopes: ["profile", "email", "openid"]
|
||||
})
|
||||
|
||||
event.cookies.set('google_oauth_state', state, {
|
||||
path: '/',
|
||||
secure: import.meta.env.PROD,
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 10,
|
||||
sameSite: 'lax',
|
||||
})
|
||||
|
||||
event.cookies.set('google_oauth_code_verifier', codeVerifier, {
|
||||
path: '/',
|
||||
secure: import.meta.env.PROD,
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 10,
|
||||
sameSite: 'lax',
|
||||
})
|
||||
|
||||
redirect(302, url.toString())
|
||||
}
|
||||
20
src/routes/(auth)/login/spotify/+server.ts
Normal file
20
src/routes/(auth)/login/spotify/+server.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { github } from '$lib/server/auth'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
import { generateState } from 'arctic'
|
||||
|
||||
import type { RequestEvent } from '@sveltejs/kit'
|
||||
|
||||
export async function GET(event: RequestEvent): Promise<Response> {
|
||||
const state = generateState()
|
||||
const url = await github.createAuthorizationURL(state)
|
||||
|
||||
event.cookies.set('github_oauth_state', state, {
|
||||
path: '/',
|
||||
secure: import.meta.env.PROD,
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 10,
|
||||
sameSite: 'lax',
|
||||
})
|
||||
|
||||
redirect(302, url.toString())
|
||||
}
|
||||
20
src/routes/(auth)/login/tidal/+server.ts
Normal file
20
src/routes/(auth)/login/tidal/+server.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { github } from '$lib/server/auth'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
import { generateState } from 'arctic'
|
||||
|
||||
import type { RequestEvent } from '@sveltejs/kit'
|
||||
|
||||
export async function GET(event: RequestEvent): Promise<Response> {
|
||||
const state = generateState()
|
||||
const url = await github.createAuthorizationURL(state)
|
||||
|
||||
event.cookies.set('github_oauth_state', state, {
|
||||
path: '/',
|
||||
secure: import.meta.env.PROD,
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 10,
|
||||
sameSite: 'lax',
|
||||
})
|
||||
|
||||
redirect(302, url.toString())
|
||||
}
|
||||
Loading…
Reference in a new issue