Merge pull request #25 from BradNut/oauth

Adding Oauth
This commit is contained in:
Bradley Shellnut 2024-09-22 21:20:12 +00:00 committed by GitHub
commit a8ceafd22c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 3149 additions and 666 deletions

View file

@ -18,6 +18,12 @@ ADMIN_PASSWORD=
TWO_FACTOR_TIMEOUT=300000 TWO_FACTOR_TIMEOUT=300000
# OAuth
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
# Public # Public
PUBLIC_SITE_NAME='Bored Game' PUBLIC_SITE_NAME='Bored Game'

21
LICENSE Normal file
View 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.

View file

@ -1,5 +1,5 @@
import 'dotenv/config' 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' import { defineConfig } from 'drizzle-kit'
export default defineConfig({ export default defineConfig({

View file

@ -27,19 +27,20 @@
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
"@melt-ui/pp": "^0.3.2", "@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.83.0", "@melt-ui/svelte": "^0.83.0",
"@playwright/test": "^1.47.0", "@playwright/test": "^1.47.2",
"@sveltejs/adapter-auto": "^3.2.4", "@sveltejs/adapter-auto": "^3.2.5",
"@sveltejs/enhanced-img": "^0.3.4", "@sveltejs/enhanced-img": "^0.3.8",
"@sveltejs/kit": "^2.5.26", "@sveltejs/kit": "^2.5.28",
"@sveltejs/vite-plugin-svelte": "^3.1.2", "@sveltejs/vite-plugin-svelte": "^3.1.2",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"@types/node": "^20.16.5", "@types/node": "^20.16.5",
"@types/pg": "^8.11.9", "@types/pg": "^8.11.10",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^7.18.0",
"arctic": "^1.9.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"drizzle-kit": "^0.23.2", "drizzle-kit": "^0.23.2",
"eslint": "^8.57.0", "eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "2.36.0-next.13", "eslint-plugin-svelte": "2.36.0-next.13",
"just-clone": "^6.2.0", "just-clone": "^6.2.0",
@ -47,13 +48,12 @@
"lucia": "3.2.0", "lucia": "3.2.0",
"lucide-svelte": "^0.408.0", "lucide-svelte": "^0.408.0",
"nodemailer": "^6.9.15", "nodemailer": "^6.9.15",
"postcss": "^8.4.45", "postcss": "^8.4.47",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-load-config": "^5.1.0", "postcss-load-config": "^5.1.0",
"postcss-preset-env": "^9.6.0", "postcss-preset-env": "^9.6.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6", "prettier-plugin-svelte": "^3.2.6",
"sass": "^1.78.0",
"satori": "^0.10.14", "satori": "^0.10.14",
"satori-html": "^0.3.2", "satori-html": "^0.3.2",
"svelte": "5.0.0-next.175", "svelte": "5.0.0-next.175",
@ -64,13 +64,13 @@
"svelte-sequential-preprocessor": "^2.0.1", "svelte-sequential-preprocessor": "^2.0.1",
"sveltekit-flash-message": "^2.4.4", "sveltekit-flash-message": "^2.4.4",
"sveltekit-rate-limiter": "^0.5.2", "sveltekit-rate-limiter": "^0.5.2",
"sveltekit-superforms": "^2.17.0", "sveltekit-superforms": "^2.19.0",
"tailwindcss": "^3.4.11", "tailwindcss": "^3.4.12",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tslib": "^2.7.0", "tslib": "^2.7.0",
"tsx": "^4.19.1", "tsx": "^4.19.1",
"typescript": "^5.6.2", "typescript": "^5.6.2",
"vite": "^5.4.4", "vite": "^5.4.7",
"vitest": "^1.6.0", "vitest": "^1.6.0",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
@ -88,14 +88,13 @@
"@neondatabase/serverless": "^0.9.5", "@neondatabase/serverless": "^0.9.5",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@resvg/resvg-js": "^2.6.2", "@resvg/resvg-js": "^2.6.2",
"@sveltejs/adapter-node": "^5.2.2", "@sveltejs/adapter-node": "^5.2.4",
"@sveltejs/adapter-vercel": "^5.4.3", "@sveltejs/adapter-vercel": "^5.4.4",
"@types/feather-icons": "^4.29.4", "@types/feather-icons": "^4.29.4",
"@vercel/og": "^0.5.20", "@vercel/og": "^0.5.20",
"arctic": "^1.9.2", "bits-ui": "^0.21.15",
"bits-ui": "^0.21.13",
"boardgamegeekclient": "^1.9.1", "boardgamegeekclient": "^1.9.1",
"bullmq": "^5.13.0", "bullmq": "^5.13.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cookie": "^0.6.0", "cookie": "^0.6.0",
@ -106,7 +105,7 @@
"feather-icons": "^4.29.2", "feather-icons": "^4.29.2",
"formsnap": "^1.0.1", "formsnap": "^1.0.1",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"hono": "^4.6.1", "hono": "^4.6.2",
"hono-rate-limiter": "^0.4.0", "hono-rate-limiter": "^0.4.0",
"html-entities": "^2.5.2", "html-entities": "^2.5.2",
"iconify-icon": "^2.1.0", "iconify-icon": "^2.1.0",
@ -116,7 +115,7 @@
"loader": "^2.1.1", "loader": "^2.1.1",
"open-props": "^1.7.6", "open-props": "^1.7.6",
"oslo": "^1.2.1", "oslo": "^1.2.1",
"pg": "^8.12.0", "pg": "^8.13.0",
"postgres": "^3.4.4", "postgres": "^3.4.4",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"radix-svelte": "^0.9.0", "radix-svelte": "^0.9.0",
@ -129,5 +128,6 @@
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tsyringe": "^4.8.0", "tsyringe": "^4.8.0",
"zod-to-json-schema": "^3.23.3" "zod-to-json-schema": "^3.23.3"
} },
"license": "MIT"
} }

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,10 @@ const EnvSchema = z.object({
DATABASE_DB: z.string(), DATABASE_DB: z.string(),
DB_MIGRATING: stringBoolean, DB_MIGRATING: stringBoolean,
DB_SEEDING: 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'), NODE_ENV: z.string().default('development'),
ORIGIN: z.string(), ORIGIN: z.string(),
PUBLIC_SITE_NAME: z.string(), PUBLIC_SITE_NAME: z.string(),

View 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'

View file

@ -18,6 +18,11 @@ export class CollectionController extends Controller {
console.log('collections service', collections) console.log('collections service', collections)
return c.json({ 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) => { .get('/:cuid', requireAuth, async (c) => {
const cuid = c.req.param('cuid') const cuid = c.req.param('cuid')
const collection = await this.collectionsService.findOneByCuid(cuid) const collection = await this.collectionsService.findOneByCuid(cuid)

View 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
}

View file

@ -0,0 +1,2 @@
ALTER TABLE "users" ADD COLUMN "email_verified" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "picture" text;

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,13 @@
"when": 1725489682980, "when": 1725489682980,
"tag": "0000_volatile_warhawk", "tag": "0000_volatile_warhawk",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1726877846811,
"tag": "0001_pink_the_enforcers",
"breakpoints": true
} }
] ]
} }

View file

@ -3,6 +3,7 @@ import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table' import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table' import { usersTable } from './users.table'
import { collection_items } from './collectionItems.table'
export const collections = pgTable('collections', { export const collections = pgTable('collections', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
@ -16,11 +17,12 @@ export const collections = pgTable('collections', {
...timestamps, ...timestamps,
}) })
export const collection_relations = relations(collections, ({ one }) => ({ export const collection_relations = relations(collections, ({ one, many }) => ({
user: one(usersTable, { user: one(usersTable, {
fields: [collections.user_id], fields: [collections.user_id],
references: [usersTable.id], references: [usersTable.id],
}), }),
collection_items: many(collection_items),
})) }))
export type Collections = InferSelectModel<typeof collections> export type Collections = InferSelectModel<typeof collections>

View file

@ -15,6 +15,8 @@ export const usersTable = pgTable('users', {
last_name: text('last_name'), last_name: text('last_name'),
verified: boolean('verified').default(false), verified: boolean('verified').default(false),
receive_email: boolean('receive_email').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), mfa_enabled: boolean('mfa_enabled').notNull().default(false),
theme: text('theme').default('system'), theme: text('theme').default('system'),
...timestamps, ...timestamps,

View file

@ -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'
@ -44,6 +45,7 @@ const routes = app
.route('/me', container.resolve(IamController).routes()) .route('/me', container.resolve(IamController).routes())
.route('/user', container.resolve(UserController).routes()) .route('/user', container.resolve(UserController).routes())
.route('/login', container.resolve(LoginController).routes()) .route('/login', container.resolve(LoginController).routes())
.route('/oauth', container.resolve(OAuthController).routes())
.route('/signup', container.resolve(SignupController).routes()) .route('/signup', container.resolve(SignupController).routes())
.route('/wishlists', container.resolve(WishlistController).routes()) .route('/wishlists', container.resolve(WishlistController).routes())
.route('/collections', container.resolve(CollectionController).routes()) .route('/collections', container.resolve(CollectionController).routes())

View file

@ -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) { async create(data: CreateCollection, db = this.drizzle.db) {
return db.insert(collections).values(data).returning().then(takeFirstOrThrow) return db.insert(collections).values(data).returning().then(takeFirstOrThrow)
} }

View file

@ -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
} }

View file

@ -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)
}
}

View file

@ -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
} }

View file

@ -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
} }

View file

@ -1,37 +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) {
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) {
if (!trx) {
return this.collectionsRepository.create({ return this.collectionsRepository.create({
user_id: userId, user_id: userId,
name: name ?? generateRandomAnimalName(), name: name ?? generateRandomAnimalName(),
}); })
}
return this.collectionsRepository.create(
{
user_id: userId,
name: name ?? generateRandomAnimalName(),
},
trx,
)
} }
} }

View 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
}
}

View file

@ -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`)
} }
// Create a UserRole entry linking the user and the role if (!trx) {
return this.userRolesRepository.create({ return this.userRolesRepository.create({
user_id: userId, user_id: userId,
role_id: role.id, role_id: role.id,
primary, primary,
}); })
}
// Create a UserRole entry linking the user and the role
return this.userRolesRepository.create(
{
user_id: userId,
role_id: role.id,
primary,
},
trx,
)
} }
} }

View file

@ -1,21 +1,28 @@
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, RoleName} 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'
import type {OAuthUser} from "$lib/server/api/common/types/oauth";
@injectable() @injectable()
export class UsersService { 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 +30,76 @@ 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) => {
const createdUser = await this.usersRepository.create(
{
first_name: firstName, first_name: firstName,
last_name: lastName, last_name: lastName,
email, email,
username, username,
}) },
trx,
)
if (!user) { if (!createdUser) {
return null return null
} }
const credentials = await this.credentialsRepository.create({ const credentials = await this.credentialsRepository.create(
user_id: user.id, {
user_id: createdUser.id,
type: CredentialsType.PASSWORD, type: CredentialsType.PASSWORD,
secret_data: hashedPassword, secret_data: hashedPassword,
}) },
trx,
)
if (!credentials) { if (!credentials) {
await this.usersRepository.delete(user.id) await this.usersRepository.delete(createdUser.id)
return null return null
} }
await this.userRolesService.addRoleToUser(user.id, 'user', true) await this.userRolesService.addRoleToUser(createdUser.id, RoleName.USER, true, trx)
await this.wishlistsService.createEmptyNoName(user.id) await this.wishlistsService.createEmptyNoName(createdUser.id, trx)
await this.collectionsService.createEmptyNoName(user.id) await this.collectionsService.createEmptyNoName(createdUser.id, trx)
})
}
return user 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,
)
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
})
} }
async updateUser(userId: string, data: UpdateUser) { async updateUser(userId: string, data: UpdateUser) {

View file

@ -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) {
if (!trx) {
return this.wishlistsRepository.create({ return this.wishlistsRepository.create({
user_id: userId, user_id: userId,
name: name ?? generateRandomAnimalName(), name: name ?? generateRandomAnimalName(),
}); })
}
return this.wishlistsRepository.create(
{
user_id: userId,
name: name ?? generateRandomAnimalName(),
},
trx,
)
} }
} }

6
src/lib/server/auth.ts Normal file
View 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`);

View file

@ -1,13 +1,12 @@
import { notSignedInMessage } from '$lib/flashMessages' 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 { db } from '$lib/server/api/packages/drizzle'
import { userNotAuthenticated } from '$lib/server/auth-utils'
import { modifyListGameSchema } from '$lib/validations/zod-schemas' import { modifyListGameSchema } from '$lib/validations/zod-schemas'
import { type Actions, error, fail } from '@sveltejs/kit' import { type Actions, error, fail } from '@sveltejs/kit'
import { and, eq } from 'drizzle-orm' import { and, eq } from 'drizzle-orm'
import { redirect } from 'sveltekit-flash-message/server' import { redirect } from 'sveltekit-flash-message/server'
import { zod } from 'sveltekit-superforms/adapters' import { zod } from 'sveltekit-superforms/adapters'
import { superValidate } from 'sveltekit-superforms/server' import { superValidate } from 'sveltekit-superforms/server'
import { collection_items, collections, gamesTable } from '../../../../lib/server/api/databases/tables'
export async function load(event) { export async function load(event) {
const { locals } = event const { locals } = event
@ -18,23 +17,10 @@ export async function load(event) {
} }
try { try {
const userCollections = await db.query.collections.findMany({ const { data, error } = await locals.api.collections.$get().then(locals.parseApiResponse)
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, {})
}
return { return {
collections: userCollections, collections: data?.collections || [],
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)

View file

@ -1,24 +1,34 @@
<script lang="ts"> <script lang="ts">
const { data } = $props(); import * as Card from '$components/ui/card'
let collections = data?.collections || []; const { data } = $props()
let collections = data?.collections || []
</script> </script>
<svelte:head> <svelte:head>
<title>Your Collections | Bored Game</title> <title>Your Collections | Bored Game</title>
</svelte:head> </svelte:head>
<div class="container">
<h1>Your Collections</h1> <h1>Your Collections</h1>
<div class="collections">
<div class="collection-list"> <div class="collection-list">
{#if collections.length === 0} {#if collections.length === 0}
<h2>You have no collections</h2> <h2>You have no collections</h2>
{:else} {:else}
{#each collections as collection} {#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> <h2><a href="/collections/{collection.cuid}">{collection.name}</a></h2>
<h3>Created at: {new Date(collection.created_at).toLocaleString()}</h3> <h3>Created at: {new Date(collection.createdAt).toLocaleString()}</h3>
</div> </div> -->
{/each} {/each}
{/if} {/if}
</div> </div>
@ -30,10 +40,6 @@
width: 100%; width: 100%;
} }
.collections {
margin: 2rem 0;
}
.collection-list { .collection-list {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(200px, 1fr)); grid-template-columns: repeat(3, minmax(200px, 1fr));

View file

@ -40,7 +40,7 @@ console.log('items', items)
<div class="games"> <div class="games">
<div class="games-list"> <div class="games-list">
{#if items.length === 0} {#if items.length === 0}
<h2>No gamesTable in your collection</h2> <h2>No games in your collection</h2>
{:else} {:else}
{#each items as game (game.game_id)} {#each items as game (game.game_id)}
<Game {game} /> <Game {game} />

View file

@ -18,14 +18,6 @@ export const load: PageServerLoad = async (event) => {
throw redirect(302, '/login', notSignedInMessage, 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), { const profileForm = await superValidate(zod(updateProfileSchema), {
defaults: { defaults: {
firstName: authedUser?.firstName ?? '', firstName: authedUser?.firstName ?? '',

View file

@ -1,5 +1,5 @@
<h1>Privacy Policy</h1> <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. 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. We collect only the personal information that is necessary for us to provide our services to you.

View 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, '/')
}

View 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, '/')
}

View file

@ -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 &&

View file

@ -1,19 +1,19 @@
<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 })),
@ -25,17 +25,17 @@
// - 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, syncFlashMessage: false,
taintedMessage: null, taintedMessage: null,
// validators: zodClient(signInSchema), // validators: zodClient(signInSchema),
// validationMethod: 'oninput', // validationMethod: 'oninput',
delayMs: 0, delayMs: 0,
}); })
const { form: loginForm, enhance } = superLoginForm; const { form: loginForm, enhance } = superLoginForm
</script> </script>
<svelte:head> <svelte:head>
@ -47,8 +47,10 @@
<Card.Header> <Card.Header>
<Card.Title class="text-2xl">Log into your account</Card.Title> <Card.Title class="text-2xl">Log into your account</Card.Title>
</Card.Header> </Card.Header>
<Card.Content> <Card.Content class="grid gap-4">
{@render usernamePasswordForm()} {@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"> <p class="px-8 py-4 text-center text-sm text-muted-foreground">
By clicking continue, you agree to our By clicking continue, you agree to our
<a href="/terms" class="underline underline-offset-4 hover:text-primary"> <a href="/terms" class="underline underline-offset-4 hover:text-primary">
@ -86,5 +88,17 @@
</form> </form>
{/snippet} {/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"> <style lang="postcss">
svg {
width: 24px;
height: 24px;
}
</style> </style>

View 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())
}

View 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())
}

View 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())
}

View 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())
}

View 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())
}