mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
commit
a6df74501b
82 changed files with 4004 additions and 1317 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({
|
||||
|
|
|
|||
46
package.json
46
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.8",
|
||||
"@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,19 +64,19 @@
|
|||
"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.10",
|
||||
"sveltekit-superforms": "^2.19.0",
|
||||
"tailwindcss": "^3.4.12",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.7.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.3",
|
||||
"tsx": "^4.19.1",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.7",
|
||||
"vitest": "^1.6.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@fontsource/fira-mono": "^5.0.14",
|
||||
"@fontsource/fira-mono": "^5.1.0",
|
||||
"@hono/swagger-ui": "^0.4.1",
|
||||
"@hono/zod-openapi": "^0.15.3",
|
||||
"@hono/zod-validator": "^0.2.2",
|
||||
|
|
@ -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.12.14",
|
||||
"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.5.11",
|
||||
"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",
|
||||
|
|
@ -128,6 +127,7 @@
|
|||
"tailwind-variants": "^0.2.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tsyringe": "^4.8.0",
|
||||
"zod-to-json-schema": "^3.23.2"
|
||||
}
|
||||
"zod-to-json-schema": "^3.23.3"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
1138
pnpm-lock.yaml
1138
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -4,15 +4,10 @@ import { invalidateAll } from '$app/navigation'
|
|||
import Logo from '$components/logo.svelte'
|
||||
import * as Avatar from '$components/ui/avatar'
|
||||
import * as DropdownMenu from '$components/ui/dropdown-menu'
|
||||
import type { Users } from '$db/schema'
|
||||
import { ListChecks, ListTodo, LogOut, User } from 'lucide-svelte'
|
||||
import { ListChecks, ListTodo, LogOut, Settings } from 'lucide-svelte'
|
||||
import toast from 'svelte-french-toast'
|
||||
|
||||
type HeaderProps = {
|
||||
user: Users | null
|
||||
}
|
||||
|
||||
let { user = null }: HeaderProps = $props()
|
||||
let { user = null } = $props()
|
||||
|
||||
let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)')
|
||||
</script>
|
||||
|
|
@ -28,6 +23,15 @@ let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)')
|
|||
</div>
|
||||
<nav>
|
||||
{#if user}
|
||||
{@render userDropdown()}
|
||||
{:else}
|
||||
<a href="/login"> <span class="flex-auto">Login</span></a>
|
||||
<a href="/signup"> <span class="flex-auto">Sign Up</span></a>
|
||||
{/if}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{#snippet userDropdown()}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Avatar.Root asChild>
|
||||
|
|
@ -42,7 +46,7 @@ let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)')
|
|||
<DropdownMenu.Separator />
|
||||
<a href="/settings">
|
||||
<DropdownMenu.Item>
|
||||
<User class="mr-2 h-4 w-4" />
|
||||
<Settings class="mr-2 h-4 w-4" />
|
||||
<span>Settings</span>
|
||||
</DropdownMenu.Item>
|
||||
</a>
|
||||
|
|
@ -90,12 +94,7 @@ let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)')
|
|||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
{:else}
|
||||
<a href="/login"> <span class="flex-auto">Login</span></a>
|
||||
<a href="/signup"> <span class="flex-auto">Sign Up</span></a>
|
||||
{/if}
|
||||
</nav>
|
||||
</header>
|
||||
{/snippet}
|
||||
|
||||
<style lang="postcss">
|
||||
header {
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import type { Route } from '$lib/types'
|
||||
|
||||
let { children, routes }: { children: unknown; routes: Route[] } = $props()
|
||||
</script>
|
||||
|
||||
<div class="mx-auto grid w-full max-w-6xl gap-2">
|
||||
<h1 class="text-3xl font-semibold">Settings</h1>
|
||||
</div>
|
||||
|
||||
<div class="security-nav">
|
||||
<nav>
|
||||
<ul>
|
||||
{#each routes as { href, label }}
|
||||
<li>
|
||||
<a href={href} class:active={$page.url.pathname.includes(href)}>
|
||||
{label}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="security-nav-content">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.security-nav {
|
||||
display: flex;
|
||||
|
||||
nav {
|
||||
width: 16rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #fff;
|
||||
padding: 1rem;
|
||||
border-right: 1px solid #ddd;
|
||||
height: 100vh;
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #337ab7;
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-link-hover);
|
||||
font-weight: 600;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.security-nav-content {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLInputAttributes } from "svelte/elements";
|
||||
import type { InputEvents } from "./index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
type $$Props = HTMLInputAttributes;
|
||||
type $$Events = InputEvents;
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import { type VariantProps, tv } from "tailwind-variants";
|
|||
import Root from "./toggle.svelte";
|
||||
|
||||
export const toggleVariants = tv({
|
||||
base: "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||
base: "ring-offset-background hover:bg-muted hover:text-muted-foreground focus-visible:ring-ring data-[state=on]:bg-accent data-[state=on]:text-accent-foreground inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
"border-input hover:bg-accent hover:text-accent-foreground border bg-transparent",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { Toggle as TogglePrimitive } from "bits-ui";
|
||||
import { type Size, type Variant, toggleVariants } from "./index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { cn } from "$lib/utils/ui.js";
|
||||
|
||||
type $$Props = TogglePrimitive.Props & {
|
||||
variant?: Variant;
|
||||
|
|
|
|||
|
|
@ -1,36 +1,37 @@
|
|||
<script lang="ts">
|
||||
import { navigating } from '$app/stores';
|
||||
import { onNavigate } from '$app/navigation';
|
||||
let visible = false;
|
||||
let progress = 0;
|
||||
let load_durations: number[] = [];
|
||||
$: average_load = load_durations.reduce((a, b) => a + b, 0) / load_durations.length;
|
||||
const increment = 1;
|
||||
import { onNavigate } from '$app/navigation'
|
||||
import { navigating } from '$app/stores'
|
||||
|
||||
let visible = false
|
||||
let progress = 0
|
||||
let load_durations: number[] = []
|
||||
$: average_load = load_durations.reduce((a, b) => a + b, 0) / load_durations.length
|
||||
const increment = 1
|
||||
onNavigate((navigation) => {
|
||||
const typical_load_time = average_load || 200; //ms
|
||||
const frequency = typical_load_time / 100;
|
||||
let start = performance.now();
|
||||
const typical_load_time = average_load || 200 //ms
|
||||
const frequency = typical_load_time / 100
|
||||
let start = performance.now()
|
||||
// Start the progress bar
|
||||
visible = true;
|
||||
progress = 0;
|
||||
visible = true
|
||||
progress = 0
|
||||
const interval = setInterval(() => {
|
||||
// Increment the progress bar
|
||||
progress += increment;
|
||||
}, frequency);
|
||||
progress += increment
|
||||
}, frequency)
|
||||
// Resolve the promise when the page is done loading
|
||||
$navigating?.complete.then(() => {
|
||||
progress = 100; // Fill out the progress bar
|
||||
clearInterval(interval);
|
||||
progress = 100 // Fill out the progress bar
|
||||
clearInterval(interval)
|
||||
// after 100 ms hide the progress bar
|
||||
setTimeout(() => {
|
||||
visible = false;
|
||||
}, 500);
|
||||
visible = false
|
||||
}, 500)
|
||||
// Log how long that one took
|
||||
const end = performance.now();
|
||||
const duration = end - start;
|
||||
load_durations = [...load_durations, duration];
|
||||
});
|
||||
});
|
||||
const end = performance.now()
|
||||
const duration = end - start
|
||||
load_durations = [...load_durations, duration]
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="progress" class:visible style:--progress={progress}>
|
||||
|
|
|
|||
|
|
@ -34,5 +34,3 @@ export const config: Config = {
|
|||
max: env.DB_MIGRATING || env.DB_SEEDING ? 1 : undefined,
|
||||
},
|
||||
}
|
||||
|
||||
console.log('config', config)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { StatusCodes } from '$lib/constants/status-codes';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import { StatusCodes } from '$lib/constants/status-codes'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
export function TooManyRequests(message: string = 'Too many requests') {
|
||||
return new HTTPException(StatusCodes.TOO_MANY_REQUESTS, { message });
|
||||
export function TooManyRequests(message = 'Too many requests') {
|
||||
return new HTTPException(StatusCodes.TOO_MANY_REQUESTS, { message })
|
||||
}
|
||||
|
||||
export function Forbidden(message: string = 'Forbidden') {
|
||||
return new HTTPException(StatusCodes.FORBIDDEN, { message });
|
||||
export function Forbidden(message = 'Forbidden') {
|
||||
return new HTTPException(StatusCodes.FORBIDDEN, { message })
|
||||
}
|
||||
|
||||
export function Unauthorized(message: string = 'Unauthorized') {
|
||||
return new HTTPException(StatusCodes.UNAUTHORIZED, { message });
|
||||
export function Unauthorized(message = 'Unauthorized') {
|
||||
return new HTTPException(StatusCodes.UNAUTHORIZED, { message })
|
||||
}
|
||||
|
||||
export function NotFound(message: string = 'Not Found') {
|
||||
return new HTTPException(StatusCodes.NOT_FOUND, { message });
|
||||
export function NotFound(message = 'Not Found') {
|
||||
return new HTTPException(StatusCodes.NOT_FOUND, { message })
|
||||
}
|
||||
|
||||
export function BadRequest(message: string = 'Bad Request') {
|
||||
return new HTTPException(StatusCodes.BAD_REQUEST, { message });
|
||||
export function BadRequest(message = 'Bad Request') {
|
||||
return new HTTPException(StatusCodes.BAD_REQUEST, { message })
|
||||
}
|
||||
|
||||
export function InternalError(message: string = 'Internal Error') {
|
||||
return new HTTPException(StatusCodes.INTERNAL_SERVER_ERROR, { message });
|
||||
export function InternalError(message = 'Internal Error') {
|
||||
return new HTTPException(StatusCodes.INTERNAL_SERVER_ERROR, { message })
|
||||
}
|
||||
|
|
|
|||
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'
|
||||
|
|
@ -2,7 +2,7 @@ import 'reflect-metadata'
|
|||
import { Controller } from '$lib/server/api/common/types/controller'
|
||||
import { CollectionsService } from '$lib/server/api/services/collections.service'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { requireAuth } from '../middleware/auth.middleware'
|
||||
import { requireAuth } from '../middleware/require-auth.middleware'
|
||||
|
||||
@injectable()
|
||||
export class CollectionController extends Controller {
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
import { StatusCodes } from '$lib/constants/status-codes'
|
||||
import { Controller } from '$lib/server/api/common/types/controller'
|
||||
import { changePasswordDto } from '$lib/server/api/dtos/change-password.dto'
|
||||
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto'
|
||||
import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto'
|
||||
import { verifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto'
|
||||
import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'
|
||||
import { IamService } from '$lib/server/api/services/iam.service'
|
||||
import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service'
|
||||
import { LuciaService } from '$lib/server/api/services/lucia.service'
|
||||
import { zValidator } from '@hono/zod-validator'
|
||||
import { setCookie } from 'hono/cookie'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { requireAuth } from '../middleware/auth.middleware'
|
||||
import { requireAuth } from '../middleware/require-auth.middleware'
|
||||
|
||||
@injectable()
|
||||
export class IamController extends Controller {
|
||||
constructor(
|
||||
@inject(IamService) private readonly iamService: IamService,
|
||||
@inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService,
|
||||
@inject(LuciaService) private luciaService: LuciaService,
|
||||
) {
|
||||
super()
|
||||
|
|
@ -45,6 +48,32 @@ export class IamController extends Controller {
|
|||
}
|
||||
return c.json({}, StatusCodes.OK)
|
||||
})
|
||||
.put('/update/password', requireAuth, zValidator('json', changePasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const user = c.var.user
|
||||
const { password, confirm_password } = c.req.valid('json')
|
||||
if (password !== confirm_password) {
|
||||
return c.json('Passwords do not match', StatusCodes.BAD_REQUEST)
|
||||
}
|
||||
try {
|
||||
await this.iamService.updatePassword(user.id, { password, confirm_password })
|
||||
await this.luciaService.lucia.invalidateUserSessions(user.id)
|
||||
await this.loginRequestService.createUserSession(user.id, c.req, undefined)
|
||||
const sessionCookie = this.luciaService.lucia.createBlankSessionCookie()
|
||||
setCookie(c, sessionCookie.name, sessionCookie.value, {
|
||||
path: sessionCookie.attributes.path,
|
||||
maxAge: sessionCookie.attributes.maxAge,
|
||||
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({ status: 'success' })
|
||||
} catch (error) {
|
||||
console.error('Error updating password', error)
|
||||
return c.json('Error updating password', StatusCodes.BAD_REQUEST)
|
||||
}
|
||||
})
|
||||
.post('/update/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const user = c.var.user
|
||||
const { email } = c.req.valid('json')
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { UsersService } from '$lib/server/api/services/users.service'
|
|||
import { zValidator } from '@hono/zod-validator'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { CredentialsType } from '../databases/tables'
|
||||
import { requireAuth } from '../middleware/auth.middleware'
|
||||
import { requireAuth } from '../middleware/require-auth.middleware'
|
||||
|
||||
@injectable()
|
||||
export class MfaController extends Controller {
|
||||
|
|
|
|||
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
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import 'reflect-metadata'
|
|||
import { Controller } from '$lib/server/api/common/types/controller'
|
||||
import { UsersService } from '$lib/server/api/services/users.service'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { requireAuth } from '../middleware/auth.middleware'
|
||||
import { requireAuth } from '../middleware/require-auth.middleware'
|
||||
|
||||
@injectable()
|
||||
export class UserController extends Controller {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import 'reflect-metadata'
|
|||
import { Controller } from '$lib/server/api/common/types/controller'
|
||||
import { WishlistsService } from '$lib/server/api/services/wishlists.service'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { requireAuth } from '../middleware/auth.middleware'
|
||||
import { requireAuth } from '../middleware/require-auth.middleware'
|
||||
|
||||
@injectable()
|
||||
export class WishlistController extends Controller {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import * as schema from '$lib/server/api/databases/tables'
|
||||
import { type db } from '$lib/server/api/packages/drizzle'
|
||||
import type { db } from '$lib/server/api/packages/drizzle'
|
||||
import roles from './data/roles.json'
|
||||
|
||||
export default async function seed(db: db) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@ import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
|
|||
import { timestamps } from '../../common/utils/table'
|
||||
import { user_roles } from './userRoles.table'
|
||||
|
||||
export enum RoleName {
|
||||
ADMIN = 'admin',
|
||||
EDITOR = 'editor',
|
||||
MODERATOR = 'moderator',
|
||||
USER = 'user',
|
||||
}
|
||||
|
||||
export const rolesTable = pgTable('roles', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
17
src/lib/server/api/dtos/change-password.dto.ts
Normal file
17
src/lib/server/api/dtos/change-password.dto.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { refinePasswords } from '$lib/validations/account'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const changePasswordDto = z
|
||||
.object({
|
||||
password: z.string({ required_error: 'Password is required' }).trim(),
|
||||
confirm_password: z
|
||||
.string({ required_error: 'Confirm Password is required' })
|
||||
.trim()
|
||||
.min(8, { message: 'Must be at least 8 characters' })
|
||||
.max(128, { message: 'Must be less than 128 characters' }),
|
||||
})
|
||||
.superRefine(({ confirm_password, password }, ctx) => {
|
||||
return refinePasswords(confirm_password, password, ctx)
|
||||
})
|
||||
|
||||
export type ChangePasswordDto = z.infer<typeof changePasswordDto>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { z } from "zod";
|
||||
import { z } from 'zod'
|
||||
|
||||
export const updateProfileDto = z.object({
|
||||
firstName: z
|
||||
|
|
@ -7,17 +7,8 @@ export const updateProfileDto = z.object({
|
|||
.min(3, { message: 'Must be at least 3 characters' })
|
||||
.max(50, { message: 'Must be less than 50 characters' })
|
||||
.optional(),
|
||||
lastName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, {message: 'Must be at least 3 characters'})
|
||||
.max(50, {message: 'Must be less than 50 characters'})
|
||||
.optional(),
|
||||
username: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, {message: 'Must be at least 3 characters'})
|
||||
.max(50, {message: 'Must be less than 50 characters'})
|
||||
});
|
||||
lastName: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }).optional(),
|
||||
username: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }),
|
||||
})
|
||||
|
||||
export type UpdateProfileDto = z.infer<typeof updateProfileDto>;
|
||||
export type UpdateProfileDto = z.infer<typeof updateProfileDto>
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ export class AuthCleanupJobs {
|
|||
this.queue = this.jobsService.createQueue('test')
|
||||
|
||||
/* ---------------------------- Register Workers ---------------------------- */
|
||||
this.worker().then((r) => console.log('auth-cleanup job worker started'))
|
||||
this.worker().then(() => console.log('auth-cleanup job worker started'))
|
||||
}
|
||||
|
||||
async deleteStaleEmailVerificationRequests() {
|
||||
await this.queue.add('delete_stale_email_verifiactions', null, {
|
||||
await this.queue.add('delete_stale_email_verifications', null, {
|
||||
repeat: {
|
||||
pattern: '0 0 * * 0', // Runs once a week at midnight on Sunday
|
||||
},
|
||||
|
|
@ -31,7 +31,7 @@ export class AuthCleanupJobs {
|
|||
|
||||
private async worker() {
|
||||
return this.jobsService.createWorker(this.queue.name, async (job) => {
|
||||
if (job.name === 'delete_stale_email_verifiactions') {
|
||||
if (job.name === 'delete_stale_email_verifications') {
|
||||
// delete stale email verifications
|
||||
}
|
||||
if (job.name === 'delete_stale_login_requests') {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import { LuciaService } from '$lib/server/api/services/lucia.service'
|
||||
import type { MiddlewareHandler } from 'hono'
|
||||
import { createMiddleware } from 'hono/factory'
|
||||
import type { Session, User } from 'lucia'
|
||||
import { verifyRequestOrigin } from 'oslo/request'
|
||||
import { container } from 'tsyringe'
|
||||
import { Unauthorized } from '../common/exceptions'
|
||||
import type { HonoTypes } from '../common/types/hono'
|
||||
|
||||
// resolve dependencies from the container
|
||||
|
|
@ -41,14 +39,3 @@ export const validateAuthSession: MiddlewareHandler<HonoTypes> = createMiddlewar
|
|||
c.set('user', user)
|
||||
return next()
|
||||
})
|
||||
|
||||
export const requireAuth: MiddlewareHandler<{
|
||||
Variables: {
|
||||
session: Session
|
||||
user: User
|
||||
}
|
||||
}> = createMiddleware(async (c, next) => {
|
||||
const user = c.var.user
|
||||
if (!user) throw Unauthorized('You must be logged in to access this resource')
|
||||
return next()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { takeFirstOrThrow } from '../common/utils/repository'
|
|||
|
||||
export type CreateCredentials = InferInsertModel<typeof credentialsTable>
|
||||
export type UpdateCredentials = Partial<CreateCredentials>
|
||||
export type DeleteCredentials = Pick<CreateCredentials, 'id'>
|
||||
|
||||
@injectable()
|
||||
export class CredentialsRepository {
|
||||
|
|
@ -43,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) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { CredentialsType } from '$lib/server/api/databases/tables'
|
||||
import type { ChangePasswordDto } from '$lib/server/api/dtos/change-password.dto'
|
||||
import type { UpdateEmailDto } from '$lib/server/api/dtos/update-email.dto'
|
||||
import type { UpdateProfileDto } from '$lib/server/api/dtos/update-profile.dto'
|
||||
import type { VerifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto'
|
||||
|
|
@ -42,13 +44,13 @@ export class IamService {
|
|||
}
|
||||
|
||||
const existingUserForNewUsername = await this.usersService.findOneByUsername(data.username)
|
||||
if (existingUserForNewUsername && existingUserForNewUsername.id !== userId) {
|
||||
if (existingUserForNewUsername && existingUserForNewUsername.id !== user.id) {
|
||||
return {
|
||||
error: 'Username already in use',
|
||||
}
|
||||
}
|
||||
|
||||
return this.usersService.updateUser(userId, {
|
||||
return this.usersService.updateUser(user.id, {
|
||||
first_name: data.firstName,
|
||||
last_name: data.lastName,
|
||||
username: data.username !== user.username ? data.username : user.username,
|
||||
|
|
@ -68,6 +70,11 @@ export class IamService {
|
|||
})
|
||||
}
|
||||
|
||||
async updatePassword(userId: string, data: ChangePasswordDto) {
|
||||
const { password } = data
|
||||
await this.usersService.updatePassword(userId, password)
|
||||
}
|
||||
|
||||
async verifyPassword(userId: string, data: VerifyPasswordDto) {
|
||||
const user = await this.usersService.findOneById(userId)
|
||||
if (!user) {
|
||||
|
|
|
|||
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(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`)
|
||||
}
|
||||
|
||||
// Create a UserRole entry linking the user and the role
|
||||
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,
|
||||
},
|
||||
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({
|
||||
return await this.drizzleService.db.transaction(async (trx) => {
|
||||
const createdUser = await this.usersRepository.create(
|
||||
{
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
email,
|
||||
username,
|
||||
})
|
||||
},
|
||||
trx,
|
||||
)
|
||||
|
||||
if (!user) {
|
||||
if (!createdUser) {
|
||||
return null
|
||||
}
|
||||
|
||||
const credentials = await this.credentialsRepository.create({
|
||||
user_id: user.id,
|
||||
const credentials = await this.credentialsRepository.create(
|
||||
{
|
||||
user_id: createdUser.id,
|
||||
type: CredentialsType.PASSWORD,
|
||||
secret_data: hashedPassword,
|
||||
})
|
||||
},
|
||||
trx,
|
||||
)
|
||||
|
||||
if (!credentials) {
|
||||
await this.usersRepository.delete(user.id)
|
||||
await this.usersRepository.delete(createdUser.id)
|
||||
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.collectionsService.createEmptyNoName(user.id)
|
||||
await this.wishlistsService.createEmptyNoName(createdUser.id, trx)
|
||||
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) {
|
||||
|
|
@ -69,6 +118,22 @@ export class UsersService {
|
|||
return this.usersRepository.findOneById(id)
|
||||
}
|
||||
|
||||
async updatePassword(userId: string, password: string) {
|
||||
const hashedPassword = await this.tokenService.createHashedToken(password)
|
||||
const currentCredentials = await this.credentialsRepository.findPasswordCredentialsByUserId(userId)
|
||||
if (!currentCredentials) {
|
||||
await this.credentialsRepository.create({
|
||||
user_id: userId,
|
||||
type: CredentialsType.PASSWORD,
|
||||
secret_data: hashedPassword,
|
||||
})
|
||||
} else {
|
||||
await this.credentialsRepository.update(currentCredentials.id, {
|
||||
secret_data: hashedPassword,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async verifyPassword(userId: string, data: { password: string }) {
|
||||
const user = await this.usersRepository.findOneById(userId)
|
||||
if (!user) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +1,38 @@
|
|||
import 'reflect-metadata';
|
||||
import { container } from 'tsyringe';
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { HashingService } from '../services/hashing.service';
|
||||
import 'reflect-metadata'
|
||||
import { container } from 'tsyringe'
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { HashingService } from '../services/hashing.service'
|
||||
|
||||
describe('HashingService', () => {
|
||||
let service: HashingService;
|
||||
let service: HashingService
|
||||
|
||||
beforeAll(() => {
|
||||
service = container.resolve(HashingService);
|
||||
});
|
||||
service = container.resolve(HashingService)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.resetAllMocks()
|
||||
});
|
||||
})
|
||||
|
||||
describe('Create Hash', () => {
|
||||
it('should create a hash', async () => {
|
||||
const hash = await service.hash('111');
|
||||
expect(hash).not.toBeUndefined();
|
||||
expect(hash).not.toBeNull();
|
||||
});
|
||||
const hash = await service.hash('111')
|
||||
expect(hash).not.toBeUndefined()
|
||||
expect(hash).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Verify Hash', () => {
|
||||
it('should verify a hash', async () => {
|
||||
const hash = await service.hash('111');
|
||||
const verifiable = await service.verify(hash, '111');
|
||||
expect(verifiable).toBeTruthy();
|
||||
});
|
||||
const hash = await service.hash('111')
|
||||
const verifiable = await service.verify(hash, '111')
|
||||
expect(verifiable).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not verify a hash', async () => {
|
||||
const hash = await service.hash('111')
|
||||
const verifiable = await service.verify(hash, '222')
|
||||
expect(verifiable).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
124
src/lib/server/api/tests/iam.service.test.ts
Normal file
124
src/lib/server/api/tests/iam.service.test.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import 'reflect-metadata'
|
||||
import { IamService } from '$lib/server/api/services/iam.service'
|
||||
import { LuciaService } from '$lib/server/api/services/lucia.service'
|
||||
import { UsersService } from '$lib/server/api/services/users.service'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { container } from 'tsyringe'
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('IamService', () => {
|
||||
let service: IamService
|
||||
const luciaService = vi.mocked(LuciaService.prototype)
|
||||
const userService = vi.mocked(UsersService.prototype)
|
||||
|
||||
beforeAll(() => {
|
||||
service = container
|
||||
.register<LuciaService>(LuciaService, { useValue: luciaService })
|
||||
.register<UsersService>(UsersService, { useValue: userService })
|
||||
.resolve(IamService)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
const timeStampDate = new Date()
|
||||
const dbUser = {
|
||||
id: faker.string.uuid(),
|
||||
cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
|
||||
first_name: faker.person.firstName(),
|
||||
last_name: faker.person.lastName(),
|
||||
email: faker.internet.email(),
|
||||
username: faker.internet.userName(),
|
||||
verified: false,
|
||||
receive_email: false,
|
||||
mfa_enabled: false,
|
||||
theme: 'system',
|
||||
createdAt: timeStampDate,
|
||||
updatedAt: timeStampDate,
|
||||
}
|
||||
|
||||
describe('Update Profile', () => {
|
||||
it('should update user', async () => {
|
||||
userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser)
|
||||
userService.findOneByUsername = vi.fn().mockResolvedValue(undefined)
|
||||
userService.updateUser = vi.fn().mockResolvedValue(dbUser)
|
||||
|
||||
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById')
|
||||
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername')
|
||||
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser')
|
||||
await expect(
|
||||
service.updateProfile(faker.string.uuid(), {
|
||||
username: faker.internet.userName(),
|
||||
}),
|
||||
).resolves.toEqual(dbUser)
|
||||
expect(spy_userService_findOneById).toBeCalledTimes(1)
|
||||
expect(spy_userService_findOneByUsername).toBeCalledTimes(1)
|
||||
expect(spy_userService_updateUser).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should error on no user found', async () => {
|
||||
userService.findOneById = vi.fn().mockResolvedValueOnce(undefined)
|
||||
|
||||
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById')
|
||||
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername')
|
||||
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser')
|
||||
await expect(
|
||||
service.updateProfile(faker.string.uuid(), {
|
||||
username: faker.internet.userName(),
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
error: 'User not found',
|
||||
})
|
||||
expect(spy_userService_findOneById).toBeCalledTimes(1)
|
||||
expect(spy_userService_findOneByUsername).toBeCalledTimes(0)
|
||||
expect(spy_userService_updateUser).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
it('should error on duplicate username', async () => {
|
||||
userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser)
|
||||
userService.findOneByUsername = vi.fn().mockResolvedValue({
|
||||
id: faker.string.uuid(),
|
||||
})
|
||||
userService.updateUser = vi.fn().mockResolvedValue(dbUser)
|
||||
|
||||
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById')
|
||||
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername')
|
||||
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser')
|
||||
await expect(
|
||||
service.updateProfile(faker.string.uuid(), {
|
||||
username: faker.internet.userName(),
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
error: 'Username already in use',
|
||||
})
|
||||
expect(spy_userService_findOneById).toBeCalledTimes(1)
|
||||
expect(spy_userService_findOneByUsername).toBeCalledTimes(1)
|
||||
expect(spy_userService_updateUser).toBeCalledTimes(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not error if the user id of new username is the current user id', async () => {
|
||||
userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser)
|
||||
userService.findOneByUsername = vi.fn().mockResolvedValue({
|
||||
id: dbUser.id,
|
||||
})
|
||||
userService.updateUser = vi.fn().mockResolvedValue(dbUser)
|
||||
|
||||
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById')
|
||||
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername')
|
||||
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser')
|
||||
await expect(
|
||||
service.updateProfile(dbUser.id, {
|
||||
username: dbUser.id,
|
||||
}),
|
||||
).resolves.toEqual(dbUser)
|
||||
expect(spy_userService_findOneById).toBeCalledTimes(1)
|
||||
expect(spy_userService_findOneByUsername).toBeCalledTimes(1)
|
||||
expect(spy_userService_updateUser).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,49 +1,45 @@
|
|||
import 'reflect-metadata';
|
||||
import { container } from 'tsyringe';
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { TokensService } from '../services/tokens.service';
|
||||
import { HashingService } from '../services/hashing.service';
|
||||
import { Argon2id } from 'oslo/password';
|
||||
import 'reflect-metadata'
|
||||
import { Argon2id } from 'oslo/password'
|
||||
import { container } from 'tsyringe'
|
||||
import { afterAll, beforeAll, describe, expect, expectTypeOf, it, vi } from 'vitest'
|
||||
import { HashingService } from '../services/hashing.service'
|
||||
import { TokensService } from '../services/tokens.service'
|
||||
|
||||
describe('TokensService', () => {
|
||||
let service: TokensService;
|
||||
const hashingService = vi.mocked(HashingService.prototype);
|
||||
let service: TokensService
|
||||
const hashingService = vi.mocked(HashingService.prototype)
|
||||
|
||||
beforeAll(() => {
|
||||
service = container
|
||||
.register<HashingService>(HashingService, { useValue: hashingService })
|
||||
.resolve(TokensService);
|
||||
});
|
||||
service = container.register<HashingService>(HashingService, { useValue: hashingService }).resolve(TokensService)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.resetAllMocks()
|
||||
});
|
||||
})
|
||||
|
||||
describe('Generate Token', () => {
|
||||
const hashedPassword = new Argon2id().hash('111');
|
||||
|
||||
hashingService.hash = vi.fn().mockResolvedValue(hashedPassword);
|
||||
hashingService.verify = vi.fn().mockResolvedValue(true);
|
||||
|
||||
const spy_hashingService_hash = vi.spyOn(hashingService, 'hash');
|
||||
const spy_hashingService_verify = vi.spyOn(hashingService, 'verify');
|
||||
|
||||
it('should resolve', async () => {
|
||||
await expect(service.createHashedToken('111')).resolves.string
|
||||
const hashedPassword = await new Argon2id().hash('111')
|
||||
hashingService.hash = vi.fn().mockResolvedValue(hashedPassword)
|
||||
const spy_hashingService_hash = vi.spyOn(hashingService, 'hash')
|
||||
const spy_hashingService_verify = vi.spyOn(hashingService, 'verify')
|
||||
await expectTypeOf(service.createHashedToken('111')).resolves.toBeString()
|
||||
expect(spy_hashingService_hash).toBeCalledTimes(1)
|
||||
expect(spy_hashingService_verify).toBeCalledTimes(0)
|
||||
})
|
||||
it('should generate a token that is verifiable', async () => {
|
||||
const token = await service.createHashedToken('111');
|
||||
expect(token).not.toBeUndefined();
|
||||
expect(token).not.toBeNull();
|
||||
const verifiable = await service.verifyHashedToken(token, '111');
|
||||
expect(verifiable).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should generate a hashed token', async () => {
|
||||
expect(spy_hashingService_hash).toHaveBeenCalledTimes(2);
|
||||
hashingService.hash = vi.fn().mockResolvedValue(await new Argon2id().hash('111'))
|
||||
hashingService.verify = vi.fn().mockResolvedValue(true)
|
||||
const spy_hashingService_hash = vi.spyOn(hashingService, 'hash')
|
||||
const spy_hashingService_verify = vi.spyOn(hashingService, 'verify')
|
||||
const token = await service.createHashedToken('111')
|
||||
expect(token).not.toBeNaN()
|
||||
expect(token).not.toBeUndefined()
|
||||
expect(token).not.toBeNull()
|
||||
const verifiable = await service.verifyHashedToken(token, '111')
|
||||
expect(verifiable).toBeTruthy()
|
||||
expect(spy_hashingService_hash).toBeCalledTimes(1)
|
||||
expect(spy_hashingService_verify).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
it('should verify a hashed token', async () => {
|
||||
expect(spy_hashingService_verify).toHaveBeenCalledTimes(1);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
@ -1,62 +1,75 @@
|
|||
import 'reflect-metadata';
|
||||
import { container } from 'tsyringe';
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { UserRolesService } from '../services/user_roles.service';
|
||||
import { UserRolesRepository } from '../repositories/user_roles.repository';
|
||||
import { RolesService } from '../services/roles.service';
|
||||
import 'reflect-metadata'
|
||||
import { RoleName } from '$lib/server/api/databases/tables'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { container } from 'tsyringe'
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { UserRolesRepository } from '../repositories/user_roles.repository'
|
||||
import { RolesService } from '../services/roles.service'
|
||||
import { UserRolesService } from '../services/user_roles.service'
|
||||
|
||||
describe('UserRolesService', () => {
|
||||
let service: UserRolesService;
|
||||
const userRolesRepository = vi.mocked(UserRolesRepository.prototype);
|
||||
const rolesService = vi.mocked(RolesService.prototype);
|
||||
let service: UserRolesService
|
||||
const userRolesRepository = vi.mocked(UserRolesRepository.prototype)
|
||||
const rolesService = vi.mocked(RolesService.prototype)
|
||||
|
||||
beforeAll(() => {
|
||||
service = container
|
||||
.register<UserRolesRepository>(UserRolesRepository, { useValue: userRolesRepository })
|
||||
.register<RolesService>(RolesService, { useValue: rolesService })
|
||||
.resolve(UserRolesService);
|
||||
});
|
||||
.resolve(UserRolesService)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.resetAllMocks()
|
||||
});
|
||||
})
|
||||
|
||||
const timeStampDate = new Date()
|
||||
const roleUUID = faker.string.uuid()
|
||||
const userUUID = faker.string.uuid()
|
||||
const dbRole = {
|
||||
id: roleUUID,
|
||||
cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
|
||||
name: RoleName.ADMIN,
|
||||
createdAt: timeStampDate,
|
||||
updatedAt: timeStampDate,
|
||||
}
|
||||
|
||||
const dbUserRole = {
|
||||
id: faker.string.uuid(),
|
||||
cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
|
||||
role_id: roleUUID,
|
||||
user_id: userUUID,
|
||||
primary: true,
|
||||
createdAt: timeStampDate,
|
||||
updatedAt: timeStampDate,
|
||||
}
|
||||
|
||||
describe('Create User Role', () => {
|
||||
rolesService.findOneByNameOrThrow = vi.fn().mockResolvedValue({
|
||||
id: '3e0e9f0f-0a0b-4f0b-8f0b-0a0b4f0b8f0b',
|
||||
cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
|
||||
name: 'user',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
} satisfies Awaited<ReturnType<typeof rolesService.findOneByNameOrThrow>>);
|
||||
|
||||
userRolesRepository.create = vi.fn().mockResolvedValue({
|
||||
id: '3e0e9f0f-0a0b-4f0b-8f0b-0a0b4f0b8f0b',
|
||||
cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
|
||||
user_id: '3e0e9f0f-0a0b-4f0b-8f0b-0a0b4f0b8fff',
|
||||
role_id: '3e0e9f0f-0a0b-4f0b-8f0b-0a0b4f0b8f0b',
|
||||
primary: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
} satisfies Awaited<ReturnType<typeof userRolesRepository.create>>);
|
||||
|
||||
const spy_rolesService_findOneByNameOrThrow = vi.spyOn(rolesService, 'findOneByNameOrThrow');
|
||||
const spy_userRolesRepository_create = vi.spyOn(userRolesRepository, 'create');
|
||||
|
||||
it('should resolve', async () => {
|
||||
await expect(service.addRoleToUser('3e0e9f0f-0a0b-4f0b-8f0b-0a0b4f0b8fff', 'user', true)).resolves.not.toThrowError();
|
||||
})
|
||||
it('should call rolesService.findOneByNameOrThrow', async () => {
|
||||
expect(spy_rolesService_findOneByNameOrThrow).toBeCalledWith('user');
|
||||
expect(spy_rolesService_findOneByNameOrThrow).toBeCalledTimes(1);
|
||||
})
|
||||
it('should call userRolesRepository.create', async () => {
|
||||
rolesService.findOneByNameOrThrow = vi.fn().mockResolvedValue(dbRole satisfies Awaited<ReturnType<typeof rolesService.findOneByNameOrThrow>>)
|
||||
|
||||
userRolesRepository.create = vi.fn().mockResolvedValue(dbUserRole satisfies Awaited<ReturnType<typeof userRolesRepository.create>>)
|
||||
|
||||
const spy_rolesService_findOneByNameOrThrow = vi.spyOn(rolesService, 'findOneByNameOrThrow')
|
||||
const spy_userRolesRepository_create = vi.spyOn(userRolesRepository, 'create')
|
||||
|
||||
await expect(service.addRoleToUser(userUUID, RoleName.ADMIN, true)).resolves.not.toThrowError()
|
||||
expect(spy_rolesService_findOneByNameOrThrow).toBeCalledWith(RoleName.ADMIN)
|
||||
expect(spy_rolesService_findOneByNameOrThrow).toBeCalledTimes(1)
|
||||
expect(spy_userRolesRepository_create).toBeCalledWith({
|
||||
user_id: '3e0e9f0f-0a0b-4f0b-8f0b-0a0b4f0b8fff',
|
||||
role_id: '3e0e9f0f-0a0b-4f0b-8f0b-0a0b4f0b8f0b',
|
||||
primary: true
|
||||
});
|
||||
expect(spy_userRolesRepository_create).toBeCalledTimes(1);
|
||||
user_id: userUUID,
|
||||
role_id: dbRole.id,
|
||||
primary: true,
|
||||
})
|
||||
expect(spy_userRolesRepository_create).toBeCalledTimes(1)
|
||||
})
|
||||
it('should error on no role found', async () => {
|
||||
rolesService.findOneByNameOrThrow = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
const spy_rolesService_findOneByNameOrThrow = vi.spyOn(rolesService, 'findOneByNameOrThrow')
|
||||
await expect(service.addRoleToUser(userUUID, RoleName.ADMIN, true)).rejects.toThrowError(`Role with name ${RoleName.ADMIN} not found`)
|
||||
expect(spy_rolesService_findOneByNameOrThrow).toBeCalledWith(RoleName.ADMIN)
|
||||
expect(spy_rolesService_findOneByNameOrThrow).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
|
|
@ -1,23 +1,25 @@
|
|||
import 'reflect-metadata';
|
||||
import { container } from 'tsyringe';
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { UsersService } from '../services/users.service';
|
||||
import { CredentialsRepository } from '../repositories/credentials.repository';
|
||||
import { TokensService } from '../services/tokens.service';
|
||||
import { UserRolesService } from '../services/user_roles.service';
|
||||
import { UsersRepository } from '../repositories/users.repository';
|
||||
import { Argon2id } from 'oslo/password';
|
||||
import { WishlistsService } from '../services/wishlists.service';
|
||||
import { CollectionsService } from '../services/collections.service';
|
||||
import 'reflect-metadata'
|
||||
import { CredentialsType } from '$lib/server/api/databases/tables'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { Argon2id } from 'oslo/password'
|
||||
import { container } from 'tsyringe'
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { CredentialsRepository } from '../repositories/credentials.repository'
|
||||
import { UsersRepository } from '../repositories/users.repository'
|
||||
import { CollectionsService } from '../services/collections.service'
|
||||
import { TokensService } from '../services/tokens.service'
|
||||
import { UserRolesService } from '../services/user_roles.service'
|
||||
import { UsersService } from '../services/users.service'
|
||||
import { WishlistsService } from '../services/wishlists.service'
|
||||
|
||||
describe('UsersService', () => {
|
||||
let service: UsersService;
|
||||
const credentialsRepository = vi.mocked(CredentialsRepository.prototype);
|
||||
const tokensService = vi.mocked(TokensService.prototype);
|
||||
const usersRepository = vi.mocked(UsersRepository.prototype);
|
||||
const userRolesService = vi.mocked(UserRolesService.prototype);
|
||||
const wishlistsService = vi.mocked(WishlistsService.prototype);
|
||||
const collectionsService = vi.mocked(CollectionsService.prototype);
|
||||
let service: UsersService
|
||||
const credentialsRepository = vi.mocked(CredentialsRepository.prototype)
|
||||
const tokensService = vi.mocked(TokensService.prototype)
|
||||
const usersRepository = vi.mocked(UsersRepository.prototype)
|
||||
const userRolesService = vi.mocked(UserRolesService.prototype)
|
||||
const wishlistsService = vi.mocked(WishlistsService.prototype)
|
||||
const collectionsService = vi.mocked(CollectionsService.prototype)
|
||||
|
||||
beforeAll(() => {
|
||||
service = container
|
||||
|
|
@ -27,78 +29,102 @@ describe('UsersService', () => {
|
|||
.register<UserRolesService>(UserRolesService, { useValue: userRolesService })
|
||||
.register<WishlistsService>(WishlistsService, { useValue: wishlistsService })
|
||||
.register<CollectionsService>(CollectionsService, { useValue: collectionsService })
|
||||
.resolve(UsersService);
|
||||
});
|
||||
.resolve(UsersService)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
describe('Create User', () => {
|
||||
const hashedPassword = new Argon2id().hash('111');
|
||||
tokensService.createHashedToken = vi.fn().mockResolvedValue(hashedPassword)
|
||||
|
||||
usersRepository.create = vi.fn().mockResolvedValue({
|
||||
id: '3e0e9f0f-0a0b-4f0b-8f0b-0a0b4f0b8f0b',
|
||||
const timeStampDate = new Date()
|
||||
const dbUser = {
|
||||
id: faker.string.uuid(),
|
||||
cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
|
||||
first_name: 'test',
|
||||
last_name: 'test',
|
||||
email: 'test@example.com',
|
||||
username: 'test',
|
||||
first_name: faker.person.firstName(),
|
||||
last_name: faker.person.lastName(),
|
||||
email: faker.internet.email(),
|
||||
username: faker.internet.userName(),
|
||||
verified: false,
|
||||
receive_email: false,
|
||||
mfa_enabled: false,
|
||||
theme: 'system',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
} satisfies Awaited<ReturnType<typeof usersRepository.create>>)
|
||||
|
||||
credentialsRepository.create = vi.fn().mockResolvedValue({
|
||||
id: '3e0e9f0f-0a0b-4f0b-8f0b-0a0b4f0b8f0b',
|
||||
user_id: '3e0e9f0f-0a0b-4f0b-8f0b-0a0b4f0b8f0b',
|
||||
type: 'PASSWORD',
|
||||
secret_data: hashedPassword
|
||||
})
|
||||
createdAt: timeStampDate,
|
||||
updatedAt: timeStampDate,
|
||||
}
|
||||
const dbCredentials = {
|
||||
id: faker.string.uuid(),
|
||||
user_id: dbUser.id,
|
||||
type: CredentialsType.PASSWORD,
|
||||
secret_data: 'hashedPassword',
|
||||
createdAt: timeStampDate,
|
||||
updatedAt: timeStampDate,
|
||||
}
|
||||
|
||||
describe('Create User', () => {
|
||||
it('should resolve', async () => {
|
||||
const hashedPassword = new Argon2id().hash('111')
|
||||
tokensService.createHashedToken = vi.fn().mockResolvedValue(hashedPassword)
|
||||
usersRepository.create = vi.fn().mockResolvedValue(dbUser satisfies Awaited<ReturnType<typeof usersRepository.create>>)
|
||||
credentialsRepository.create = vi.fn().mockResolvedValue(dbCredentials satisfies Awaited<ReturnType<typeof credentialsRepository.create>>)
|
||||
userRolesService.addRoleToUser = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
wishlistsService.createEmptyNoName = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
collectionsService.createEmptyNoName = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
const spy_tokensService_createHashToken = vi.spyOn(tokensService, 'createHashedToken');
|
||||
const spy_usersRepository_create = vi.spyOn(usersRepository, 'create');
|
||||
const spy_credentialsRepository_create = vi.spyOn(credentialsRepository, 'create');
|
||||
const spy_userRolesService_addRoleToUser = vi.spyOn(userRolesService, 'addRoleToUser');
|
||||
const spy_wishlistsService_createEmptyNoName = vi.spyOn(wishlistsService, 'createEmptyNoName');
|
||||
const spy_collectionsService_createEmptyNoName = vi.spyOn(collectionsService, 'createEmptyNoName');
|
||||
|
||||
it('should resolve', async () => {
|
||||
await expect(service.create({
|
||||
firstName: 'test',
|
||||
lastName: 'test',
|
||||
email: 'test@example.com',
|
||||
username: 'test',
|
||||
password: '111',
|
||||
confirm_password: '111'
|
||||
})).resolves.not.toThrow()
|
||||
})
|
||||
it('should generate a hashed token', async () => {
|
||||
const spy_tokensService_createHashToken = vi.spyOn(tokensService, 'createHashedToken')
|
||||
const spy_usersRepository_create = vi.spyOn(usersRepository, 'create')
|
||||
const spy_credentialsRepository_create = vi.spyOn(credentialsRepository, 'create')
|
||||
const spy_userRolesService_addRoleToUser = vi.spyOn(userRolesService, 'addRoleToUser')
|
||||
const spy_wishlistsService_createEmptyNoName = vi.spyOn(wishlistsService, 'createEmptyNoName')
|
||||
const spy_collectionsService_createEmptyNoName = vi.spyOn(collectionsService, 'createEmptyNoName')
|
||||
await expect(
|
||||
service.create({
|
||||
firstName: faker.person.firstName(),
|
||||
lastName: faker.person.lastName(),
|
||||
email: faker.internet.email(),
|
||||
username: faker.internet.userName(),
|
||||
password: faker.string.alphanumeric(10),
|
||||
confirm_password: faker.string.alphanumeric(10),
|
||||
}),
|
||||
).resolves.toEqual(dbUser)
|
||||
expect(spy_tokensService_createHashToken).toBeCalledTimes(1)
|
||||
})
|
||||
it('should create a new user', async () => {
|
||||
expect(spy_usersRepository_create).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
it('should create a new credential', async () => {
|
||||
expect(spy_usersRepository_create).toBeCalledTimes(1)
|
||||
expect(spy_credentialsRepository_create).toBeCalledTimes(1)
|
||||
})
|
||||
it('should add role to user', async () => {
|
||||
expect(spy_userRolesService_addRoleToUser).toBeCalledTimes(1)
|
||||
})
|
||||
it('should create a new wishlist', async () => {
|
||||
expect(spy_wishlistsService_createEmptyNoName).toBeCalledTimes(1)
|
||||
})
|
||||
it('should create a new collection', async () => {
|
||||
expect(spy_collectionsService_createEmptyNoName).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
});
|
||||
describe('Update User', () => {
|
||||
it('should resolve Password Exiting Credentials', async () => {
|
||||
const hashedPassword = new Argon2id().hash('111')
|
||||
tokensService.createHashedToken = vi.fn().mockResolvedValue(hashedPassword)
|
||||
credentialsRepository.update = vi.fn().mockResolvedValue(dbCredentials satisfies Awaited<ReturnType<typeof credentialsRepository.update>>)
|
||||
credentialsRepository.findPasswordCredentialsByUserId = vi
|
||||
.fn()
|
||||
.mockResolvedValue(dbCredentials satisfies Awaited<ReturnType<typeof credentialsRepository.findPasswordCredentialsByUserId>>)
|
||||
|
||||
const spy_tokensService_createHashToken = vi.spyOn(tokensService, 'createHashedToken')
|
||||
const spy_credentialsRepository_findPasswordCredentialsByUserId = vi.spyOn(credentialsRepository, 'findPasswordCredentialsByUserId')
|
||||
const spy_credentialsRepository_update = vi.spyOn(credentialsRepository, 'update')
|
||||
await expect(service.updatePassword(dbUser.id, faker.string.alphanumeric(10))).resolves.toBeUndefined()
|
||||
expect(spy_tokensService_createHashToken).toBeCalledTimes(1)
|
||||
expect(spy_credentialsRepository_findPasswordCredentialsByUserId).toBeCalledTimes(1)
|
||||
expect(spy_credentialsRepository_update).toBeCalledTimes(1)
|
||||
})
|
||||
it('Should Create User Password No Existing Credentials', async () => {
|
||||
const hashedPassword = new Argon2id().hash('111')
|
||||
tokensService.createHashedToken = vi.fn().mockResolvedValue(hashedPassword)
|
||||
credentialsRepository.findPasswordCredentialsByUserId = vi.fn().mockResolvedValue(null)
|
||||
credentialsRepository.create = vi.fn().mockResolvedValue(dbCredentials satisfies Awaited<ReturnType<typeof credentialsRepository.create>>)
|
||||
|
||||
const spy_tokensService_createHashToken = vi.spyOn(tokensService, 'createHashedToken')
|
||||
const spy_credentialsRepository_create = vi.spyOn(credentialsRepository, 'create')
|
||||
const spy_credentialsRepository_findPasswordCredentialsByUserId = vi.spyOn(credentialsRepository, 'findPasswordCredentialsByUserId')
|
||||
|
||||
await expect(service.updatePassword(dbUser.id, faker.string.alphanumeric(10))).resolves.not.toThrow()
|
||||
expect(spy_tokensService_createHashToken).toBeCalledTimes(1)
|
||||
expect(spy_credentialsRepository_findPasswordCredentialsByUserId).toBeCalledTimes(1)
|
||||
expect(spy_credentialsRepository_create).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
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));
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import { notSignedInMessage } from '$lib/flashMessages.js'
|
||||
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 { type Actions, error } 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 { params, locals } = event
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
import Game from '$components/Game.svelte'
|
||||
import type { UICollection } from '$lib/types'
|
||||
|
||||
export let data
|
||||
const { data } = $props()
|
||||
const { items = [] } = data
|
||||
console.log(`Page data: ${JSON.stringify(data)}`)
|
||||
let collection: UICollection = data?.collection ?? {}
|
||||
let items = data?.items || []
|
||||
console.log('items', items)
|
||||
|
||||
// async function handleNextPageEvent(event: CustomEvent) {
|
||||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import LeftNav from '$components/LeftNav.svelte'
|
||||
import { page } from '$app/stores'
|
||||
import type { Route } from '$lib/types'
|
||||
|
||||
const routes: Route[] = [
|
||||
|
|
@ -10,6 +10,74 @@ const routes: Route[] = [
|
|||
let { children } = $props()
|
||||
</script>
|
||||
|
||||
<LeftNav {routes}>
|
||||
<div class="security-nav">
|
||||
<nav>
|
||||
<div class="mx-auto w-full max-w-6xl mb-2">
|
||||
<h1 class="text-3xl font-semibold">Settings</h1>
|
||||
</div>
|
||||
<ul>
|
||||
{#each routes as { href, label }}
|
||||
<li>
|
||||
<a href={href} class:active={$page.url.pathname.includes(href)}>
|
||||
{label}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="security-nav-content">
|
||||
{@render children()}
|
||||
</LeftNav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.security-nav {
|
||||
display: flex;
|
||||
|
||||
nav {
|
||||
width: 16rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #fff;
|
||||
padding: 1rem;
|
||||
border-right: 1px solid #ddd;
|
||||
height: 100vh;
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #337ab7;
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-link-hover);
|
||||
font-weight: 600;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.security-nav-content {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
margin: 0 auto;
|
||||
max-width: 80vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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 ?? '',
|
||||
|
|
|
|||
|
|
@ -48,53 +48,26 @@ export const actions: Actions = {
|
|||
})
|
||||
}
|
||||
|
||||
console.log('updating profile')
|
||||
if (!event.locals.user) {
|
||||
redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
|
||||
if (!event.locals.session) {
|
||||
return fail(401)
|
||||
}
|
||||
|
||||
const dbUser = await db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.id, authedUser.id),
|
||||
const { error: verifyPasswordError } = await locals.api.me.verify.password
|
||||
.$post({
|
||||
json: { password: form.data.current_password },
|
||||
})
|
||||
.then(locals.parseApiResponse)
|
||||
|
||||
// if (!dbUser?.hashed_password) {
|
||||
// form.data.password = '';
|
||||
// form.data.confirm_password = '';
|
||||
// form.data.current_password = '';
|
||||
// return setError(
|
||||
// form,
|
||||
// 'Error occurred. Please try again or contact support if you need further help.',
|
||||
// );
|
||||
// }
|
||||
console.log('verifyPasswordError', verifyPasswordError)
|
||||
|
||||
const currentPasswordVerified = await new Argon2id().verify(
|
||||
// dbUser.hashed_password,
|
||||
form.data.current_password,
|
||||
)
|
||||
|
||||
if (!currentPasswordVerified) {
|
||||
if (verifyPasswordError) {
|
||||
console.error(verifyPasswordError)
|
||||
return setError(form, 'current_password', 'Your password is incorrect')
|
||||
}
|
||||
if (authedUser?.username) {
|
||||
let sessionCookie: Cookie
|
||||
try {
|
||||
if (form.data.password !== form.data.confirm_password) {
|
||||
return setError(form, 'Password and confirm password do not match')
|
||||
}
|
||||
const hashedPassword = await new Argon2id().hash(form.data.password)
|
||||
await lucia.invalidateUserSessions(authedUser.id)
|
||||
// await db
|
||||
// .update(usersTable)
|
||||
// .set({ hashed_password: hashedPassword })
|
||||
// .where(eq(usersTable.id, user.id));
|
||||
await lucia.createSession(user.id, {
|
||||
country: event.locals.session?.ipCountry ?? 'unknown',
|
||||
await locals.api.me.update.password.$put({
|
||||
json: { password: form.data.password, confirm_password: form.data.confirm_password },
|
||||
})
|
||||
sessionCookie = lucia.createBlankSessionCookie()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
form.data.password = ''
|
||||
|
|
@ -102,11 +75,6 @@ export const actions: Actions = {
|
|||
form.data.current_password = ''
|
||||
return setError(form, 'current_password', 'Your password is incorrect.')
|
||||
}
|
||||
event.cookies.set(sessionCookie.name, sessionCookie.value, {
|
||||
path: '.',
|
||||
...sessionCookie.attributes,
|
||||
})
|
||||
|
||||
const message = {
|
||||
type: 'success',
|
||||
message: 'Password Updated. Please sign in.',
|
||||
|
|
@ -114,10 +82,5 @@ export const actions: Actions = {
|
|||
redirect(302, '/login', message, event)
|
||||
}
|
||||
return setError(form, 'Error occurred. Please try again or contact support if you need further help.')
|
||||
// TODO: Add toast instead?
|
||||
// form.data.password = '';
|
||||
// form.data.confirm_password = '';
|
||||
// form.data.current_password = '';
|
||||
// return message(form, 'Profile updated successfully.');
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
import * as Alert from '$components/ui/alert'
|
||||
import * as Form from '$components/ui/form'
|
||||
import { Input } from '$components/ui/input'
|
||||
import { AlertTriangle } from 'lucide-svelte'
|
||||
import { Toggle } from '$components/ui/toggle'
|
||||
import { AlertTriangle, EyeIcon, EyeOff } from 'lucide-svelte'
|
||||
import { zodClient } from 'sveltekit-superforms/adapters'
|
||||
import { superForm } from 'sveltekit-superforms/client'
|
||||
import { changeUserPasswordSchema } from './schemas'
|
||||
|
|
@ -16,37 +17,85 @@ const form = superForm(data.form, {
|
|||
multipleSubmits: 'prevent',
|
||||
})
|
||||
|
||||
let hiddenCurrentPassword = $state(true)
|
||||
let hiddenPassword = $state(true)
|
||||
let hiddenConfirmPassword = $state(true)
|
||||
let currentPasswordInput = $derived(hiddenCurrentPassword ? 'password' : 'text')
|
||||
let passwordInput = $derived(hiddenPassword ? 'password' : 'text')
|
||||
let confirmPasswordInput = $derived(hiddenConfirmPassword ? 'password' : 'text')
|
||||
|
||||
// $inspect(hiddenCurrentPassword, hiddenPassword, hiddenConfirmPassword)
|
||||
|
||||
const { form: formData, enhance } = form
|
||||
</script>
|
||||
|
||||
<form method="POST" use:enhance>
|
||||
<h3>Change Password</h3>
|
||||
<hr class="!border-t-2 mt-2 mb-6" />
|
||||
<Alert.Root variant="destructive">
|
||||
<label for="username" class="sr-only">Username</label>
|
||||
<input type="text" id="username" name="username" aria-hidden="true" class="hidden" autocomplete="username">
|
||||
<Alert.Root variant="destructive" class="mb-4">
|
||||
<AlertTriangle class="h-4 w-4" />
|
||||
<Alert.Title>Heads up!</Alert.Title>
|
||||
<Alert.Description>
|
||||
Changing your password will log you out of all devices.
|
||||
</Alert.Description>
|
||||
<Alert.Description>Changing your password will log you out of all devices.</Alert.Description>
|
||||
</Alert.Root>
|
||||
<Form.Field {form} name="current_password">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label for="current_password">Current Password</Form.Label>
|
||||
<Input {...attrs} bind:value={$formData.current_password} />
|
||||
<span class="flex gap-1">
|
||||
<Input
|
||||
{...attrs}
|
||||
autocomplete="password"
|
||||
type={currentPasswordInput}
|
||||
bind:value={$formData.current_password}
|
||||
/>
|
||||
<Toggle
|
||||
aria-label={`${hiddenCurrentPassword ? 'Show' : 'Hide'} Current Password}`}
|
||||
onPressedChange={() => (hiddenCurrentPassword = !hiddenCurrentPassword)}
|
||||
>
|
||||
{#if hiddenCurrentPassword}<EyeIcon />{:else}<EyeOff />{/if}
|
||||
</Toggle>
|
||||
</span>
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="password">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label for="password">New Password</Form.Label>
|
||||
<Input {...attrs} bind:value={$formData.password} />
|
||||
<span class="flex gap-1">
|
||||
<Input
|
||||
{...attrs}
|
||||
autocomplete="new-password"
|
||||
type={passwordInput}
|
||||
bind:value={$formData.password}
|
||||
/>
|
||||
<Toggle
|
||||
aria-label={`${hiddenPassword ? 'Show' : 'Hide'} Password}`}
|
||||
onPressedChange={() => (hiddenPassword = !hiddenPassword)}
|
||||
>
|
||||
{#if hiddenPassword}<EyeIcon />{:else}<EyeOff />{/if}
|
||||
</Toggle>
|
||||
</span>
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="confirm_password">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label for="confirm_password">Confirm New Password</Form.Label>
|
||||
<Input {...attrs} bind:value={$formData.confirm_password} />
|
||||
<span class="flex gap-1">
|
||||
<Input
|
||||
{...attrs}
|
||||
autocomplete="new-password"
|
||||
type={confirmPasswordInput}
|
||||
bind:value={$formData.confirm_password}
|
||||
/>
|
||||
<Toggle
|
||||
aria-label={`${hiddenConfirmPassword ? 'Show' : 'Hide'} Confirm Password}`}
|
||||
onPressedChange={() => (hiddenConfirmPassword = !hiddenConfirmPassword)}
|
||||
>
|
||||
{#if hiddenConfirmPassword}<EyeIcon />{:else}<EyeOff />{/if}
|
||||
</Toggle>
|
||||
</span>
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ export const changeUserPasswordSchema = z
|
|||
export type ChangeUserPasswordSchema = typeof changeUserPasswordSchema
|
||||
|
||||
const refinePasswords = async (confirm_password: string, password: string, ctx: z.RefinementCtx) => {
|
||||
comparePasswords(confirm_password, password, ctx)
|
||||
checkPasswordStrength(password, ctx)
|
||||
await comparePasswords(confirm_password, password, ctx)
|
||||
await checkPasswordStrength(password, ctx)
|
||||
}
|
||||
|
||||
const comparePasswords = async (confirm_password: string, password: string, ctx: z.RefinementCtx) => {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,6 @@
|
|||
import { notSignedInMessage } from '$lib/flashMessages'
|
||||
import env from '$lib/server/api/common/env'
|
||||
import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account'
|
||||
import { type Actions, fail } from '@sveltejs/kit'
|
||||
import kebabCase from 'just-kebab-case'
|
||||
import { base32, decodeHex } from 'oslo/encoding'
|
||||
import { createTOTPKeyURI } from 'oslo/otp'
|
||||
import QRCode from 'qrcode'
|
||||
import type { Actions } from '@sveltejs/kit'
|
||||
import { redirect } from 'sveltekit-flash-message/server'
|
||||
import { zod } from 'sveltekit-superforms/adapters'
|
||||
import { setError, superValidate } from 'sveltekit-superforms/server'
|
||||
import type { PageServerLoad } from '../../$types'
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
|
|
@ -19,7 +11,14 @@ export const load: PageServerLoad = async (event) => {
|
|||
throw redirect(302, '/login', notSignedInMessage, event)
|
||||
}
|
||||
|
||||
return {}
|
||||
const { data: totpData, error: totpDataError } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse)
|
||||
|
||||
const totpEnabled = !!totpData
|
||||
|
||||
return {
|
||||
totpEnabled,
|
||||
hardwareTokenEnabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
export const actions: Actions = {}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import * as Card from '$lib/components/ui/card'
|
|||
|
||||
const { data } = $props()
|
||||
|
||||
const totpEnabled = true
|
||||
const hardwareTokenEnabled = true
|
||||
const totpEnabled = data.totpEnabled
|
||||
const hardwareTokenEnabled = data.hardwareTokenEnabled
|
||||
</script>
|
||||
|
||||
<h1>Two-factor authentication</h1>
|
||||
|
|
@ -19,7 +19,7 @@ const hardwareTokenEnabled = true
|
|||
<section>
|
||||
<div class="two-factor-method">
|
||||
<div class="two-factor-method-content">
|
||||
<h2>Authenticator app {#if hardwareTokenEnabled}<Badge variant="outline" className="text-green-500 border-green-500">Configured</Badge>{/if}</h2>
|
||||
<h2>Authenticator app {#if totpEnabled}<Badge variant="outline" className="text-green-500 border-green-500">Configured</Badge>{/if}</h2>
|
||||
<p>Use an authenticator app or browser extension to get two-factor authentication codes when prompted.</p>
|
||||
</div>
|
||||
<Button href="/settings/security/mfa/totp">Edit</Button>
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export const actions: Actions = {
|
|||
|
||||
const { error: verifyPasswordError } = await locals.api.me.verify.password
|
||||
.$post({
|
||||
json: { password: addTwoFactorForm.data.current_password },
|
||||
json: { password: addTwoFactorForm.data.password },
|
||||
})
|
||||
.then(locals.parseApiResponse)
|
||||
|
||||
|
|
@ -144,7 +144,7 @@ export const actions: Actions = {
|
|||
}
|
||||
const { error: verifyPasswordError } = await locals.api.me.verify.password
|
||||
.$post({
|
||||
json: { password: removeTwoFactorForm.data.current_password },
|
||||
json: { password: removeTwoFactorForm.data.password },
|
||||
})
|
||||
.then(locals.parseApiResponse)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const { form: removeTwoFactorFormData, enhance: removeTwoFactorEnhance } = remov
|
|||
<Form.Field form={removeTwoFactorForm} name="current_password">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label for="password">Current Password</Form.Label>
|
||||
<Input type="password" {...attrs} bind:value={$removeTwoFactorFormData.current_password} />
|
||||
<Input type="password" {...attrs} bind:value={$removeTwoFactorFormData.password} autocomplete="password" />
|
||||
</Form.Control>
|
||||
<Form.Description>Please enter your current password.</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
|
|
@ -64,7 +64,7 @@ const { form: removeTwoFactorFormData, enhance: removeTwoFactorEnhance } = remov
|
|||
<Form.Field form={addTwoFactorForm} name="current_password">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label for="password">Enter Password</Form.Label>
|
||||
<Input type="password" {...attrs} bind:value={$addTwoFactorFormData.current_password} />
|
||||
<Input type="password" {...attrs} bind:value={$addTwoFactorFormData.password} autocomplete="password" />
|
||||
</Form.Control>
|
||||
<Form.Description>Please enter your current password.</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { z } from 'zod'
|
||||
|
||||
export const addTwoFactorSchema = z.object({
|
||||
current_password: z.string({ required_error: 'Current Password is required' }),
|
||||
password: z.string({ required_error: 'Current Password is required' }),
|
||||
two_factor_code: z.string({ required_error: 'Two Factor Code is required' }).trim(),
|
||||
})
|
||||
|
||||
export type AddTwoFactorSchema = typeof addTwoFactorSchema
|
||||
|
||||
export const removeTwoFactorSchema = addTwoFactorSchema.pick({
|
||||
current_password: true,
|
||||
password: true,
|
||||
})
|
||||
|
||||
export type RemoveTwoFactorSchema = typeof removeTwoFactorSchema
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<script lang="ts">
|
||||
export let data
|
||||
let wishlistsTable = data?.wishlists || []
|
||||
const { data } = $props()
|
||||
const { wishlists = [] } = data
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Your Wishlists | Bored Game</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<h1>Your wishlistsTable</h1>
|
||||
|
||||
<div class="wishlists">
|
||||
|
|
@ -23,6 +24,7 @@ let wishlistsTable = data?.wishlists || []
|
|||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
h1 {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
<script lang="ts">
|
||||
import Game from '$components/Game.svelte'
|
||||
|
||||
export let data
|
||||
console.log('data', data)
|
||||
const items = data.items || []
|
||||
const { data } = $props()
|
||||
const { items = [] } = data
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{`Your Wishlist | Bored Game`}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<h1>Your wishlist</h1>
|
||||
|
||||
<div class="games-list">
|
||||
|
|
@ -18,18 +18,27 @@ const items = data.items || []
|
|||
<Game game={item.game} />
|
||||
{/each}
|
||||
{:else}
|
||||
<h2>Sorry no gamesTable found!</h2>
|
||||
<h2>Sorry no games found!</h2>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
h1 {
|
||||
margin: 1.5rem 0rem;
|
||||
width: 100%;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
max-width: 900px;
|
||||
gap: 0.25rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.games {
|
||||
margin: 2rem 0rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.games-list {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,12 @@
|
|||
import { loadFlash } from 'sveltekit-flash-message/server';
|
||||
import type { LayoutServerLoad } from '../$types';
|
||||
// import { userFullyAuthenticated, userNotFullyAuthenticated } from '$lib/server/auth-utils';
|
||||
// import { lucia } from '$lib/server/auth';
|
||||
import { loadFlash } from 'sveltekit-flash-message/server'
|
||||
import type { LayoutServerLoad } from '../$types'
|
||||
|
||||
export const load: LayoutServerLoad = loadFlash(async (event) => {
|
||||
const { url, locals, cookies } = event;
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
|
||||
// if (userNotFullyAuthenticated(user, session)) {
|
||||
// await lucia.invalidateSession(locals.session!.id!);
|
||||
// const sessionCookie = lucia.createBlankSessionCookie();
|
||||
// cookies.set(sessionCookie.name, sessionCookie.value, {
|
||||
// path: '.',
|
||||
// ...sessionCookie.attributes,
|
||||
// });
|
||||
// }
|
||||
const { url, locals } = event
|
||||
const authedUser = await locals.getAuthedUser()
|
||||
|
||||
return {
|
||||
url: url.pathname,
|
||||
// user: userFullyAuthenticated(user, session) ? locals.user : null,
|
||||
user: authedUser,
|
||||
};
|
||||
});
|
||||
authedUser,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,37 +1,39 @@
|
|||
<script lang="ts">
|
||||
import 'iconify-icon';
|
||||
import Header from '$components/Header.svelte';
|
||||
import Footer from '$components/Footer.svelte';
|
||||
import 'iconify-icon'
|
||||
import Footer from '$components/Footer.svelte'
|
||||
import Header from '$components/Header.svelte'
|
||||
|
||||
const { data, children } = $props();
|
||||
|
||||
console.log('layout data user', data.user);
|
||||
const { data, children } = $props()
|
||||
</script>
|
||||
|
||||
<Header user={data.user} />
|
||||
<div class="flex min-h-screen w-full flex-col">
|
||||
<Header user={data.authedUser} />
|
||||
|
||||
<main>
|
||||
<main
|
||||
class="flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 p-4 md:gap-8 md:p-10"
|
||||
>
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 850px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 0rem;
|
||||
max-width: 80vw;
|
||||
/*main {*/
|
||||
/* flex: 1;*/
|
||||
/* display: flex;*/
|
||||
/* flex-direction: column;*/
|
||||
/* max-width: 850px;*/
|
||||
/* margin: 0 auto;*/
|
||||
/* padding: 2rem 0rem;*/
|
||||
/* max-width: 80vw;*/
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
max-width: 70vw;
|
||||
}
|
||||
/* @media (min-width: 1600px) {*/
|
||||
/* max-width: 70vw;*/
|
||||
/* }*/
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* box-sizing: border-box;*/
|
||||
/*}*/
|
||||
|
||||
:global(.dialog-overlay) {
|
||||
position: fixed;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import { db } from '$lib/server/api/packages/drizzle'
|
||||
import { fail } from '@sveltejs/kit'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { MetaTagsProps } from 'svelte-meta-tags'
|
||||
import { collections, usersTable, wishlistsTable } from '../../lib/server/api/databases/tables'
|
||||
import type { PageServerLoad } from './$types'
|
||||
// import { userFullyAuthenticated } from '$lib/server/auth-utils';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { locals, url } = event
|
||||
|
|
@ -61,5 +57,7 @@ export const load: PageServerLoad = async (event) => {
|
|||
}
|
||||
}
|
||||
|
||||
console.log('Not Authed')
|
||||
|
||||
return { metaTagsChild: metaTags, user: null, wishlists: [], collections: [] }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ const welcomeName = $derived.by(() => {
|
|||
|
||||
return welcomeName
|
||||
})
|
||||
|
||||
$inspect(data)
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
|
|
@ -53,5 +51,7 @@ $inspect(data)
|
|||
place-content: center;
|
||||
max-width: 900px;
|
||||
gap: 0.25rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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,19 +1,19 @@
|
|||
<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 })),
|
||||
|
|
@ -25,17 +25,17 @@
|
|||
// - 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 });
|
||||
}
|
||||
flashMessage.set({ type: 'error', message: errorMessage })
|
||||
},
|
||||
},
|
||||
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())
|
||||
}
|
||||
|
|
@ -73,21 +73,6 @@ onNavigate(async (navigation) => {
|
|||
{/if}
|
||||
|
||||
<MetaTags {...metaTags} />
|
||||
|
||||
<PageLoadingIndicator />
|
||||
|
||||
<div class="layout">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<Toaster />
|
||||
<!-- <Loading /> -->
|
||||
|
||||
<style lang="postcss">
|
||||
.layout {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -16,7 +16,8 @@ export default defineConfig({
|
|||
sveltekit()
|
||||
],
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||
mockReset: true,
|
||||
},
|
||||
css: {
|
||||
devSourcemap: true,
|
||||
|
|
|
|||
Loading…
Reference in a new issue