Fixing loading the wishlist and collection on the main landing page and getting your wishlist by id.

This commit is contained in:
Bradley Shellnut 2024-08-22 19:26:22 -07:00
parent 940b485273
commit ab4c019406
30 changed files with 684 additions and 458 deletions

View file

@ -6,7 +6,7 @@
"indentStyle": "tab", "indentStyle": "tab",
"indentWidth": 2, "indentWidth": 2,
"lineEnding": "lf", "lineEnding": "lf",
"lineWidth": 100, "lineWidth": 150,
"attributePosition": "auto", "attributePosition": "auto",
"ignore": [ "ignore": [
"**/.DS_Store", "**/.DS_Store",
@ -34,6 +34,9 @@
"bracketSameLine": false, "bracketSameLine": false,
"quoteStyle": "single", "quoteStyle": "single",
"attributePosition": "auto" "attributePosition": "auto"
},
"parser": {
"unsafeParameterDecoratorsEnabled": true
} }
}, },
"overrides": [ "overrides": [

View file

@ -30,10 +30,10 @@
"@playwright/test": "^1.46.1", "@playwright/test": "^1.46.1",
"@sveltejs/adapter-auto": "^3.2.4", "@sveltejs/adapter-auto": "^3.2.4",
"@sveltejs/enhanced-img": "^0.3.3", "@sveltejs/enhanced-img": "^0.3.3",
"@sveltejs/kit": "^2.5.22", "@sveltejs/kit": "^2.5.24",
"@sveltejs/vite-plugin-svelte": "^3.1.1", "@sveltejs/vite-plugin-svelte": "^3.1.2",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"@types/node": "^20.16.0", "@types/node": "^20.16.1",
"@types/pg": "^8.11.6", "@types/pg": "^8.11.6",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^7.18.0",
@ -57,7 +57,7 @@
"satori": "^0.10.14", "satori": "^0.10.14",
"satori-html": "^0.3.2", "satori-html": "^0.3.2",
"svelte": "5.0.0-next.175", "svelte": "5.0.0-next.175",
"svelte-check": "^3.8.5", "svelte-check": "^3.8.6",
"svelte-headless-table": "^0.18.2", "svelte-headless-table": "^0.18.2",
"svelte-meta-tags": "^3.1.3", "svelte-meta-tags": "^3.1.3",
"svelte-preprocess": "^6.0.2", "svelte-preprocess": "^6.0.2",
@ -70,13 +70,15 @@
"tslib": "^2.6.3", "tslib": "^2.6.3",
"tsx": "^4.17.0", "tsx": "^4.17.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.4.1", "vite": "^5.4.2",
"vitest": "^1.6.0", "vitest": "^1.6.0",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^5.0.14", "@fontsource/fira-mono": "^5.0.14",
"@hono/swagger-ui": "^0.4.0",
"@hono/zod-openapi": "^0.15.3",
"@hono/zod-validator": "^0.2.2", "@hono/zod-validator": "^0.2.2",
"@iconify-icons/line-md": "^1.2.30", "@iconify-icons/line-md": "^1.2.30",
"@iconify-icons/mdi": "^1.2.48", "@iconify-icons/mdi": "^1.2.48",
@ -93,17 +95,18 @@
"arctic": "^1.9.2", "arctic": "^1.9.2",
"bits-ui": "^0.21.13", "bits-ui": "^0.21.13",
"boardgamegeekclient": "^1.9.1", "boardgamegeekclient": "^1.9.1",
"bullmq": "^5.12.9", "bullmq": "^5.12.10",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cookie": "^0.6.0", "cookie": "^0.6.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"dotenv-expand": "^11.0.6", "dotenv-expand": "^11.0.6",
"drizzle-orm": "^0.32.2", "drizzle-orm": "^0.32.2",
"drizzle-zod": "^0.5.1",
"feather-icons": "^4.29.2", "feather-icons": "^4.29.2",
"formsnap": "^1.0.1", "formsnap": "^1.0.1",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"hono": "^4.5.6", "hono": "^4.5.8",
"hono-rate-limiter": "^0.4.0", "hono-rate-limiter": "^0.4.0",
"html-entities": "^2.5.2", "html-entities": "^2.5.2",
"iconify-icon": "^2.1.0", "iconify-icon": "^2.1.0",

File diff suppressed because it is too large Load diff

9
src/app.d.ts vendored
View file

@ -1,6 +1,6 @@
import { ApiClient } from './lib/server/api';
import type { User } from 'lucia'; import type { User } from 'lucia';
import { parseApiResponse } from '$lib/utils/api'; import type { ApiClient } from '$lib/server/api';
import type { parseApiResponse } from '$lib/utils/api';
// See https://kit.svelte.dev/docs/types#app // See https://kit.svelte.dev/docs/types#app
// for information about these interfaces // for information about these interfaces
@ -41,9 +41,12 @@ declare global {
interface Document { interface Document {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
startViewTransition: (callback: any) => void; // Add your custom property/method here // biome-ignore lint/suspicious/noExplicitAny: <explanation>
startViewTransition: (callback: never) => void; // Add your custom property/method here
} }
} }
// THIS IS IMPORTANT!!! // THIS IS IMPORTANT!!!
// biome-ignore lint/complexity/noUselessEmptyExport: <explanation>
// biome-ignore lint/style/useExportType: <explanation>
export {}; export {};

View file

@ -28,7 +28,7 @@ const apiClient: Handle = async ({ event, resolve }) => {
/* ----------------------------- Auth functions ----------------------------- */ /* ----------------------------- Auth functions ----------------------------- */
async function getAuthedUser() { async function getAuthedUser() {
const { data } = await api.user.$get().then(parseApiResponse) const { data } = await api.user.$get().then(parseApiResponse)
return data && data.user; return data?.user;
} }
async function getAuthedUserOrThrow() { async function getAuthedUserOrThrow() {

View file

@ -0,0 +1,5 @@
import { z } from "zod";
export const IdParamsDto = z.object({
id: z.trim().number(),
});

View file

@ -0,0 +1,31 @@
import 'reflect-metadata';
import { Hono } from 'hono';
import { inject, injectable } from 'tsyringe';
import { requireAuth } from "../middleware/auth.middleware";
import type { HonoTypes } from '../types';
import type { Controller } from '../interfaces/controller.interface';
import {CollectionsService} from "$lib/server/api/services/collections.service";
@injectable()
export class CollectionController implements Controller {
controller = new Hono<HonoTypes>();
constructor(
@inject(CollectionsService) private readonly collectionsService: CollectionsService,
) { }
routes() {
return this.controller
.get('/', requireAuth, async (c) => {
const user = c.var.user;
const collections = await this.collectionsService.findAllByUserId(user.id);
console.log('collections service', collections)
return c.json({ collections });
})
.get('/:cuid', requireAuth, async (c) => {
const cuid = c.req.param('cuid');
const user = await this.collectionsService.findOneByCuid(cuid);
return c.json({ user });
});
}
}

View file

@ -1,48 +1,54 @@
import { Hono } from 'hono'; import { Hono } from 'hono'
import { inject, injectable } from 'tsyringe'; import { inject, injectable } from 'tsyringe'
import { setCookie } from 'hono/cookie'; import { setCookie } from 'hono/cookie'
import { zValidator } from '@hono/zod-validator'; import { zValidator } from '@hono/zod-validator'
import type { HonoTypes } from '../types'; import type { HonoTypes } from '../types'
import { requireAuth } from "../middleware/auth.middleware"; import { requireAuth } from '../middleware/auth.middleware'
import type { Controller } from '../interfaces/controller.interface'; import type { Controller } from '$lib/server/api/interfaces/controller.interface'
import {IamService} from "$lib/server/api/services/iam.service"; import { IamService } from '$lib/server/api/services/iam.service'
import {LuciaProvider} from "$lib/server/api/providers"; import { LuciaProvider } from '$lib/server/api/providers'
import {limiter} from "$lib/server/api/middleware/rate-limiter.middleware"; import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'
import {updateProfileDto} from "$lib/dtos/update-profile.dto"; import { updateProfileDto } from '$lib/dtos/update-profile.dto'
import {updateEmailDto} from "$lib/dtos/update-email.dto"; import { updateEmailDto } from '$lib/dtos/update-email.dto'
import { StatusCodes } from '$lib/constants/status-codes'
@injectable() @injectable()
export class IamController implements Controller { export class IamController implements Controller {
controller = new Hono<HonoTypes>(); controller = new Hono<HonoTypes>()
constructor( constructor(
@inject(IamService) private readonly iamService: IamService, @inject(IamService) private readonly iamService: IamService,
@inject(LuciaProvider) private lucia: LuciaProvider @inject(LuciaProvider) private lucia: LuciaProvider,
) {} ) {}
routes() { routes() {
return this.controller return this.controller
.get('/me', requireAuth, async (c) => { .get('/', requireAuth, async (c) => {
const user = c.var.user; const user = c.var.user
return c.json({ user }); return c.json({ user })
}) })
.post('/update/profile', requireAuth, zValidator('json', updateProfileDto), limiter({ limit: 10, minutes: 60 }), async (c) => { .put('/update/profile', requireAuth, zValidator('json', updateProfileDto), limiter({ limit: 30, minutes: 60 }), async (c) => {
const user = c.var.user; const user = c.var.user
console.log('user id', user.id); const { firstName, lastName, username } = c.req.valid('json')
const { firstName, lastName, username } = c.req.valid('json'); const updatedUser = await this.iamService.updateProfile(user.id, { firstName, lastName, username })
const updatedUser = await this.iamService.updateProfile(user.id, { firstName, lastName, username }); if (!updatedUser) {
return c.json({ status: 'success' }); return c.json("Username already in use", StatusCodes.BAD_REQUEST);
}
return c.json({ user: updatedUser }, StatusCodes.OK)
}) })
.post('/update/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { .post('/update/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const user = c.var.user; const user = c.var.user
const { email } = c.req.valid('json'); const { email } = c.req.valid('json')
await this.iamService.updateEmail(user.id, { email }); const updatedUser = await this.iamService.updateEmail(user.id, { email })
return c.json({ status: 'success' }); if (!updatedUser) {
return c.json("Email already in use", StatusCodes.BAD_REQUEST);
}
return c.json({ user: updatedUser }, StatusCodes.OK)
}) })
.post('/logout', requireAuth, async (c) => { .post('/logout', requireAuth, async (c) => {
const sessionId = c.var.session.id; const sessionId = c.var.session.id
await this.iamService.logout(sessionId); await this.iamService.logout(sessionId)
const sessionCookie = this.lucia.createBlankSessionCookie(); const sessionCookie = this.lucia.createBlankSessionCookie()
setCookie(c, sessionCookie.name, sessionCookie.value, { setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path, path: sessionCookie.attributes.path,
maxAge: sessionCookie.attributes.maxAge, maxAge: sessionCookie.attributes.maxAge,
@ -50,9 +56,9 @@ export class IamController implements Controller {
sameSite: sessionCookie.attributes.sameSite as any, sameSite: sessionCookie.attributes.sameSite as any,
secure: sessionCookie.attributes.secure, secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly, httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires expires: sessionCookie.attributes.expires,
}); })
return c.json({ status: 'success' }); return c.json({ status: 'success' })
}); })
} }
} }

View file

@ -1,15 +1,17 @@
import 'reflect-metadata'; import 'reflect-metadata';
import { Hono } from 'hono'; import { Hono } from 'hono';
import { injectable } from 'tsyringe'; import { inject, injectable } from 'tsyringe';
import { requireAuth } from "../middleware/auth.middleware"; import { requireAuth } from "../middleware/auth.middleware";
import type { HonoTypes } from '../types'; import type { HonoTypes } from '../types';
import type { Controller } from '../interfaces/controller.interface'; import type { Controller } from '../interfaces/controller.interface';
import {UsersService} from "$lib/server/api/services/users.service";
@injectable() @injectable()
export class UserController implements Controller { export class UserController implements Controller {
controller = new Hono<HonoTypes>(); controller = new Hono<HonoTypes>();
constructor( constructor(
@inject(UsersService) private readonly usersService: UsersService
) { } ) { }
routes() { routes() {
@ -20,12 +22,12 @@ export class UserController implements Controller {
}) })
.get('/:id', requireAuth, async (c) => { .get('/:id', requireAuth, async (c) => {
const id = c.req.param('id'); const id = c.req.param('id');
const user = c.var.user; const user = await this.usersService.findOneById(id);
return c.json({ user }); return c.json({ user });
}) })
.get('/username/:userName', requireAuth, async (c) => { .get('/username/:userName', requireAuth, async (c) => {
const userName = c.req.param('userName'); const userName = c.req.param('userName');
const user = c.var.user; const user = await this.usersService.findOneByUsername(userName);
return c.json({ user }); return c.json({ user });
}); });
} }

View file

@ -0,0 +1,31 @@
import 'reflect-metadata';
import { Hono } from 'hono';
import { inject, injectable } from 'tsyringe';
import { requireAuth } from "../middleware/auth.middleware";
import type { HonoTypes } from '../types';
import type { Controller } from '../interfaces/controller.interface';
import {WishlistsService} from "$lib/server/api/services/wishlists.service";
@injectable()
export class WishlistController implements Controller {
controller = new Hono<HonoTypes>();
constructor(
@inject(WishlistsService) private readonly wishlistsService: WishlistsService
) { }
routes() {
return this.controller
.get('/', requireAuth, async (c) => {
const user = c.var.user;
const wishlists = await this.wishlistsService.findAllByUserId(user.id);
return c.json({ wishlists });
})
.get('/:cuid', requireAuth, async (c) => {
const cuid = c.req.param('cuid')
console.log(cuid)
const wishlist = await this.wishlistsService.findOneByCuid(cuid)
return c.json({ wishlist });
});
}
}

View file

@ -10,6 +10,8 @@ import { IamController } from './controllers/iam.controller';
import { LoginController } from './controllers/login.controller'; import { LoginController } from './controllers/login.controller';
import {UserController} from "$lib/server/api/controllers/user.controller"; import {UserController} from "$lib/server/api/controllers/user.controller";
import {SignupController} from "$lib/server/api/controllers/signup.controller"; import {SignupController} from "$lib/server/api/controllers/signup.controller";
import {WishlistController} from "$lib/server/api/controllers/wishlist.controller";
import {CollectionController} from "$lib/server/api/controllers/collection.controller";
/* ----------------------------------- Api ---------------------------------- */ /* ----------------------------------- Api ---------------------------------- */
const app = new Hono().basePath('/api'); const app = new Hono().basePath('/api');
@ -40,6 +42,8 @@ const routes = app
.route('/user', container.resolve(UserController).routes()) .route('/user', container.resolve(UserController).routes())
.route('/login', container.resolve(LoginController).routes()) .route('/login', container.resolve(LoginController).routes())
.route('/signup', container.resolve(SignupController).routes()) .route('/signup', container.resolve(SignupController).routes())
.route('/wishlists', container.resolve(WishlistController).routes())
.route('/collections', container.resolve(CollectionController).routes())
.get('/', (c) => c.json({ message: 'Server is healthy' })); .get('/', (c) => c.json({ message: 'Server is healthy' }));
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */

View file

@ -21,8 +21,8 @@ export const lucia = new Lucia(adapter, {
// ...attributes, // ...attributes,
username: attributes.username, username: attributes.username,
email: attributes.email, email: attributes.email,
firstName: attributes.firstName, firstName: attributes.first_name,
lastName: attributes.lastName, lastName: attributes.last_name,
theme: attributes.theme, theme: attributes.theme,
}; };
}, },
@ -54,8 +54,8 @@ declare module 'lucia' {
interface DatabaseUserAttributes { interface DatabaseUserAttributes {
username: string; username: string;
email: string; email: string;
firstName: string; first_name: string;
lastName: string; last_name: string;
theme: string; theme: string;
} }
} }

View file

@ -0,0 +1,25 @@
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
import type { z } from 'zod'
import { collections } from '$lib/server/api/infrastructure/database/tables'
export const InsertCollectionSchema = createInsertSchema(collections, {
name: (schema) => schema.name.trim()
.min(3, { message: 'Must be at least 3 characters' })
.max(64, { message: 'Must be less than 64 characters' }).optional(),
}).omit({
id: true,
cuid: true,
createdAt: true,
updatedAt: true,
})
export type InsertCollectionSchema = z.infer<typeof InsertCollectionSchema>
export const SelectCollectionSchema = createSelectSchema(collections).omit({
id: true,
user_id: true,
createdAt: true,
updatedAt: true,
})
export type SelectUserSchema = z.infer<typeof SelectCollectionSchema>

View file

@ -0,0 +1,24 @@
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
import type { z } from 'zod'
import { usersTable } from '$lib/server/api/infrastructure/database/tables'
export const InsertUserSchema = createInsertSchema(usersTable, {
email: (schema) => schema.email.max(64).email().optional(),
username: (schema) =>
schema.username.min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }).optional(),
first_name: (schema) =>
schema.first_name.trim().min(3, { message: 'Must be at least 3 characters' }).max(64, { message: 'Must be less than 64 characters' }).optional(),
last_name: (schema) =>
schema.last_name.trim().min(3, { message: 'Must be at least 3 characters' }).max(64, { message: 'Must be less than 64 characters' }).optional(),
}).omit({
id: true,
cuid: true,
createdAt: true,
updatedAt: true,
})
export type InsertUserSchema = z.infer<typeof InsertUserSchema>
export const SelectUserSchema = createSelectSchema(usersTable)
export type SelectUserSchema = z.infer<typeof SelectUserSchema>

View file

@ -1,6 +1,6 @@
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core'; import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm'; import { type InferSelectModel, relations } from 'drizzle-orm';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { timestamps } from '../utils'; import { timestamps } from '../utils';
import {user_roles} from './userRoles'; import {user_roles} from './userRoles';

View file

@ -17,12 +17,36 @@ export class CollectionsRepository {
async findOneById(id: string) { async findOneById(id: string) {
return this.db.query.collections.findFirst({ return this.db.query.collections.findFirst({
where: eq(collections.id, id) where: eq(collections.id, id),
columns: {
cuid: true,
name: true
}
})
}
async findOneByCuid(cuid: string) {
return this.db.query.collections.findFirst({
where: eq(collections.cuid, cuid),
columns: {
cuid: true,
name: true
}
}) })
} }
async findOneByUserId(userId: string) { async findOneByUserId(userId: string) {
return this.db.query.collections.findFirst({ return this.db.query.collections.findFirst({
where: eq(collections.user_id, userId),
columns: {
cuid: true,
name: true
}
})
}
async findAllByUserId(userId: string) {
return this.db.query.collections.findMany({
where: eq(collections.user_id, userId) where: eq(collections.user_id, userId)
}) })
} }

View file

@ -17,19 +17,41 @@ export class WishlistsRepository {
async findOneById(id: string) { async findOneById(id: string) {
return this.db.query.wishlists.findFirst({ return this.db.query.wishlists.findFirst({
where: eq(wishlists.id, id) where: eq(wishlists.id, id),
columns: {
cuid: true,
name: true
}
})
}
async findOneByCuid(cuid: string) {
return this.db.query.wishlists.findFirst({
where: eq(wishlists.cuid, cuid),
columns: {
cuid: true,
name: true
}
}) })
} }
async findOneByUserId(userId: string) { async findOneByUserId(userId: string) {
return this.db.query.wishlists.findFirst({ return this.db.query.wishlists.findFirst({
where: eq(wishlists.user_id, userId) where: eq(wishlists.user_id, userId),
columns: {
cuid: true,
name: true
}
}) })
} }
async findAllByUserId(userId: string) { async findAllByUserId(userId: string) {
return this.db.query.wishlists.findMany({ return this.db.query.wishlists.findMany({
where: eq(wishlists.user_id, userId) where: eq(wishlists.user_id, userId),
columns: {
cuid: true,
name: true
}
}) })
} }

View file

@ -8,6 +8,22 @@ export class CollectionsService {
@inject(CollectionsRepository) private readonly collectionsRepository: CollectionsRepository @inject(CollectionsRepository) private readonly collectionsRepository: CollectionsRepository
) { } ) { }
async findOneByUserId(userId: string) {
return this.collectionsRepository.findOneByUserId(userId);
}
async findAllByUserId(userId: string) {
return this.collectionsRepository.findAllByUserId(userId);
}
async findOneById(id: string) {
return this.collectionsRepository.findOneById(id);
}
async findOneByCuid(cuid: string) {
return this.collectionsRepository.findOneByCuid(cuid);
}
async createEmptyNoName(userId: string) { async createEmptyNoName(userId: string) {
return this.createEmpty(userId, null); return this.createEmpty(userId, null);
} }

View file

@ -1,8 +1,8 @@
import { inject, injectable } from 'tsyringe';
import { LuciaProvider } from '../providers/lucia.provider';
import {UsersService} from "$lib/server/api/services/users.service";
import type {UpdateProfileDto} from "$lib/dtos/update-profile.dto";
import type { UpdateEmailDto } from "$lib/dtos/update-email.dto"; import type { UpdateEmailDto } from "$lib/dtos/update-email.dto";
import type { UpdateProfileDto } from "$lib/dtos/update-profile.dto";
import { UsersService } from "$lib/server/api/services/users.service";
import { inject, injectable } from 'tsyringe';
import { LuciaProvider } from '$lib/server/api/providers';
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Service */ /* Service */
@ -55,8 +55,15 @@ export class IamService {
} }
async updateEmail(userId: string, data: UpdateEmailDto) { async updateEmail(userId: string, data: UpdateEmailDto) {
const { email } = data;
const existingUserEmail = await this.usersService.findOneByEmail(email);
if (existingUserEmail && existingUserEmail.id !== userId) {
return null;
}
return this.usersService.updateUser(userId, { return this.usersService.updateUser(userId, {
email: data.email email,
}); });
} }
} }

View file

@ -61,6 +61,10 @@ export class UsersService {
return this.usersRepository.findOneByUsername(username); return this.usersRepository.findOneByUsername(username);
} }
async findOneByEmail(email: string) {
return this.usersRepository.findOneByEmail(email);
}
async findOneById(id: string) { async findOneById(id: string) {
return this.usersRepository.findOneById(id); return this.usersRepository.findOneById(id);
} }

View file

@ -13,6 +13,14 @@ export class WishlistsService {
return this.wishlistsRepository.findAllByUserId(userId); return this.wishlistsRepository.findAllByUserId(userId);
} }
async findOneById(id: string) {
return this.wishlistsRepository.findOneById(id);
}
async findOneByCuid(cuid: string) {
return this.wishlistsRepository.findOneByCuid(cuid);
}
async createEmptyNoName(userId: string) { async createEmptyNoName(userId: string) {
return this.createEmpty(userId, null); return this.createEmpty(userId, null);
} }

View file

@ -4,7 +4,7 @@ export async function parseApiResponse<T>(response: ClientResponse<T>) {
if (response.status === 204 || response.headers.get('Content-Length') === '0') { if (response.status === 204 || response.headers.get('Content-Length') === '0') {
return response.ok return response.ok
? { data: null, error: null, response } ? { data: null, error: null, response }
: { data: null, error: 'An unknown error has occured', response }; : { data: null, error: 'An unknown error has occurred', response };
} }
if (response.ok) { if (response.ok) {

View file

@ -28,7 +28,7 @@ export function IntegerString<schema extends ZodNumber | ZodOptional<ZodNumber>>
return z.preprocess( return z.preprocess(
(value) => (value) =>
typeof value === 'string' typeof value === 'string'
? parseInt(value, 10) ? Number.parseInt(value, 10)
: typeof value === 'number' : typeof value === 'number'
? value ? value
: undefined, : undefined,

View file

@ -20,18 +20,19 @@ export const load: PageServerLoad = async (event) => {
if (!authedUser) { if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event); throw redirect(302, '/login', notSignedInMessage, event);
} }
console.log('authedUser', authedUser);
// if (userNotAuthenticated(user, session)) { // if (userNotAuthenticated(user, session)) {
// redirect(302, '/login', notSignedInMessage, event); // redirect(302, '/login', notSignedInMessage, event);
// } // }
//
// const dbUser = await db.query.usersTable.findFirst({ // const dbUser = await db.query.usersTable.findFirst({
// where: eq(usersTable.id, user!.id!), // where: eq(usersTable.id, user!.id!),
// }); // });
const profileForm = await superValidate(zod(profileSchema), { const profileForm = await superValidate(zod(profileSchema), {
defaults: { defaults: {
firstName: authedUser?.first_name ?? '', firstName: authedUser?.firstName ?? '',
lastName: authedUser?.last_name ?? '', lastName: authedUser?.lastName ?? '',
username: authedUser?.username ?? '', username: authedUser?.username ?? '',
}, },
}); });
@ -72,8 +73,11 @@ export const actions: Actions = {
const form = await superValidate(event, zod(updateProfileDto)); const form = await superValidate(event, zod(updateProfileDto));
const { error } = await locals.api.user.$post({ json: form.data }).then(locals.parseApiResponse); const { error } = await locals.api.me.update.profile.$put({ json: form.data }).then(locals.parwseApiResponse);
if (error) return setError(form, 'username', error); console.log('data from profile update', error);
if (error) {
return setError(form, 'username', error);
}
if (!form.valid) { if (!form.valid) {
return fail(400, { return fail(400, {
@ -81,36 +85,6 @@ export const actions: Actions = {
}); });
} }
try {
console.log('updating profile');
const user = event.locals.user;
const newUsername = form.data.username;
const existingUser = await db.query.usersTable.findFirst({
where: eq(usersTable.username, newUsername),
});
if (existingUser && existingUser.id !== user.id) {
return setError(form, 'username', 'That username is already taken');
}
await db
.update(usersTable)
.set({
first_name: form.data.firstName,
last_name: form.data.lastName,
username: form.data.username,
})
.where(eq(usersTable.id, user.id));
} catch (e) {
// @ts-expect-error
if (e.message === `AUTH_INVALID_USER_ID`) {
// invalid user id
console.error(e);
}
return setError(form, 'There was a problem updating your profile.');
}
console.log('profile updated successfully'); console.log('profile updated successfully');
return message(form, { type: 'success', message: 'Profile updated successfully!' }); return message(form, { type: 'success', message: 'Profile updated successfully!' });
}, },

View file

@ -4,23 +4,33 @@ import { zod } from 'sveltekit-superforms/adapters';
import { superValidate } from 'sveltekit-superforms/server'; import { superValidate } from 'sveltekit-superforms/server';
import { redirect } from 'sveltekit-flash-message/server'; import { redirect } from 'sveltekit-flash-message/server';
import { modifyListGameSchema } from '$lib/validations/zod-schemas'; import { modifyListGameSchema } from '$lib/validations/zod-schemas';
import db from '../../../../../db'; import { db } from '$lib/server/api/infrastructure/database';
import { notSignedInMessage } from '$lib/flashMessages.js'; import { notSignedInMessage } from '$lib/flashMessages.js';
import { games, wishlist_items, wishlists } from '$db/schema'; import { games, wishlist_items, wishlists } from '$lib/server/api/infrastructure/database/tables';
import { userNotAuthenticated } from '$lib/server/auth-utils'; import { userNotAuthenticated } from '$lib/server/auth-utils';
export async function load(event) { export async function load(event) {
const { params, locals } = event; const { params, locals } = event;
const { user, session } = locals; const { cuid } = params;
const { id } = params;
if (userNotAuthenticated(user, session)) { const authedUser = await locals.getAuthedUser();
redirect(302, '/login', notSignedInMessage, event); if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event);
} }
try { try {
const wishlist = await db.query.wishlists.findMany({ const { data, errors } = await locals.api.wishlists[':cuid'].$get({
where: and(eq(wishlists.user_id, user!.id!), eq(wishlists.cuid, id)), param: { cuid }
}); }).then(locals.parseApiResponse);
// const wishlist = await db.query.wishlists.findMany({
// where: and(eq(wishlists.user_id, authedUser.id), eq(wishlists.cuid, cuid)),
// });
if (errors) {
return error(500, 'Failed to fetch wishlist');
}
console.log('data', data);
const { wishlist } = data;
console.log('wishlist', wishlist);
if (!wishlist) { if (!wishlist) {
redirect(302, '/404'); redirect(302, '/404');

View file

@ -1,3 +1,4 @@
import { fail } from '@sveltejs/kit';
import type { MetaTagsProps } from 'svelte-meta-tags'; import type { MetaTagsProps } from 'svelte-meta-tags';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
@ -43,37 +44,24 @@ export const load: PageServerLoad = async (event) => {
}); });
if (authedUser) { if (authedUser) {
const dbUser = await db.query.usersTable.findFirst({ const { data: wishlistsData, error: wishlistsError } = await locals.api.wishlists.$get().then(locals.parseApiResponse);
where: eq(usersTable.id, authedUser!.id!), const { data: collectionsData, error: collectionsError } = await locals.api.collections.$get().then(locals.parseApiResponse);
});
console.log('Sending back user details'); if (wishlistsError || collectionsError) {
const userWishlists = await db.query.wishlists.findMany({ return fail(500, 'Failed to fetch wishlists or collections');
columns: { }
cuid: true,
name: true,
},
where: eq(wishlists.user_id, authedUser!.id!),
});
const userCollection = await db.query.collections.findMany({
columns: {
cuid: true,
name: true,
},
where: eq(collections.user_id, authedUser!.id!),
});
console.log('Wishlists', userWishlists); console.log('Wishlists', wishlistsData.wishlists);
console.log('Collections', userCollection); console.log('Collections', collectionsData.collections);
return { return {
metaTagsChild: metaTags, metaTagsChild: metaTags,
user: { user: {
firstName: dbUser?.first_name, firstName: authedUser?.firstName,
lastName: dbUser?.last_name, lastName: authedUser?.lastName,
username: dbUser?.username, username: authedUser?.username,
}, },
wishlists: userWishlists, wishlists: wishlistsData.wishlists,
collections: userCollection, collections: collectionsData.collections,
}; };
} }

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { page } from "$app/stores"; import { page } from '$app/stores';
import { Button } from "$lib/components/ui/button"; import { Button } from '$lib/components/ui/button';
import Logo from "$lib/components/logo.svelte"; import Logo from '$lib/components/logo.svelte';
import Transition from '$lib/components/transition.svelte'; import Transition from '$lib/components/transition.svelte';
let { data, children } = $props(); let { data, children } = $props();
@ -15,21 +15,11 @@
Bored Game Bored Game
</a> </a>
<div class="auth-buttons"> <div class="auth-buttons">
{#if $page.url.pathname !== "/login"} {#if $page.url.pathname !== '/login'}
<Button <Button href="/login" variant="ghost">Login</Button>
href="/login"
variant="ghost"
>
Login
</Button>
{/if} {/if}
{#if $page.url.pathname !== "/sign-up"} {#if $page.url.pathname !== '/sign-up'}
<Button <Button href="/sign-up" variant="ghost">Sign up</Button>
href="/sign-up"
variant="ghost"
>
Sign up
</Button>
{/if} {/if}
</div> </div>
<div class="auth-marketing"> <div class="auth-marketing">
@ -42,7 +32,8 @@
<div class="quote-wrapper"> <div class="quote-wrapper">
<blockquote class="quote"> <blockquote class="quote">
<p> <p>
"How many games do I own? What was the last one I played? What haven't I played in a long time? If this sounds like you then Bored Game is your new best friend." "How many games do I own? What was the last one I played? What haven't I played in a long
time? If this sounds like you then Bored Game is your new best friend."
</p> </p>
<footer>Bradley</footer> <footer>Bradley</footer>
</blockquote> </blockquote>
@ -63,7 +54,7 @@
min-height: 100vh; min-height: 100vh;
@media (width >= 768px) { @media (width >= 768px) {
display: grid display: grid;
} }
@media (width >= 1024px) { @media (width >= 1024px) {
padding-left: 0; padding-left: 0;
@ -152,7 +143,6 @@
@media (width <= 768px) { @media (width <= 768px) {
position: absolute; position: absolute;
} }
@media (width > 768px) { @media (width > 768px) {

View file

@ -1,23 +1,23 @@
import { fail, type Actions } from '@sveltejs/kit'; import { fail, type Actions } from '@sveltejs/kit'
import { eq, or } from 'drizzle-orm'; import { eq, or } from 'drizzle-orm'
import { Argon2id } from 'oslo/password'; import { Argon2id } from 'oslo/password'
import { zod } from 'sveltekit-superforms/adapters'; import { zod } from 'sveltekit-superforms/adapters'
import { setError, superValidate } from 'sveltekit-superforms/server'; import { setError, superValidate } from 'sveltekit-superforms/server'
import { redirect } from 'sveltekit-flash-message/server'; import { redirect } from 'sveltekit-flash-message/server'
import { db } from '../../../lib/server/api/infrastructure/database/index'; import { db } from '../../../lib/server/api/infrastructure/database/index'
import { lucia } from '../../../lib/server/api/infrastructure/auth/lucia'; import { lucia } from '../../../lib/server/api/infrastructure/auth/lucia'
import { credentialsTable, usersTable } from '../../../lib/server/api/infrastructure/database/tables'; import { credentialsTable, usersTable } from '../../../lib/server/api/infrastructure/database/tables'
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types'
import {signinUsernameDto} from "$lib/dtos/signin-username.dto"; import { signinUsernameDto } from '$lib/dtos/signin-username.dto'
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event; const { locals } = event
const authedUser = await locals.getAuthedUser(); const authedUser = await locals.getAuthedUser()
if (authedUser) { if (authedUser) {
const message = { type: 'success', message: 'You are already signed in' } as const; const message = { type: 'success', message: 'You are already signed in' } as const
throw redirect('/', message, event); throw redirect('/', message, event)
} }
// if (userFullyAuthenticated(user, session)) { // if (userFullyAuthenticated(user, session)) {
@ -31,34 +31,34 @@ export const load: PageServerLoad = async (event) => {
// ...sessionCookie.attributes, // ...sessionCookie.attributes,
// }); // });
// } // }
const form = await superValidate(event, zod(signinUsernameDto)); const form = await superValidate(event, zod(signinUsernameDto))
return { return {
form, form,
}; }
}; }
export const actions: Actions = { export const actions: Actions = {
default: async (event) => { default: async (event) => {
const { locals } = event; const { locals } = event
const authedUser = await locals.getAuthedUser(); const authedUser = await locals.getAuthedUser()
if (authedUser) { if (authedUser) {
const message = { type: 'success', message: 'You are already signed in' } as const; const message = { type: 'success', message: 'You are already signed in' } as const
throw redirect('/', message, event); throw redirect('/', message, event)
} }
const form = await superValidate(event, zod(signinUsernameDto)); const form = await superValidate(event, zod(signinUsernameDto))
const { error } = await locals.api.login.$post({ json: form.data }).then(locals.parseApiResponse); const { error } = await locals.api.login.$post({ json: form.data }).then(locals.parseApiResponse)
if (error) return setError(form, 'username', error); if (error) return setError(form, 'username', error)
if (!form.valid) { if (!form.valid) {
form.data.password = ''; form.data.password = ''
return fail(400, { return fail(400, {
form, form,
}); })
} }
// let session; // let session;
@ -125,9 +125,9 @@ export const actions: Actions = {
// console.log('logging in session cookie', sessionCookie); // console.log('logging in session cookie', sessionCookie);
} catch (e) { } catch (e) {
// TODO: need to return error message to the client // TODO: need to return error message to the client
console.error(e); console.error(e)
form.data.password = ''; form.data.password = ''
return setError(form, '', 'Your username or password is incorrect.'); return setError(form, '', 'Your username or password is incorrect.')
} }
// console.log('setting session cookie', sessionCookie); // console.log('setting session cookie', sessionCookie);
@ -136,8 +136,8 @@ export const actions: Actions = {
// ...sessionCookie.attributes, // ...sessionCookie.attributes,
// }); // });
form.data.username = ''; form.data.username = ''
form.data.password = ''; form.data.password = ''
// if ( // if (
// twoFactorDetails?.enabled && // twoFactorDetails?.enabled &&
@ -152,4 +152,4 @@ export const actions: Actions = {
// redirect(302, '/', message, event); // redirect(302, '/', message, event);
// } // }
}, },
}; }

View file

@ -87,25 +87,4 @@
{/snippet} {/snippet}
<style lang="postcss"> <style lang="postcss">
.login {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
flex-direction: column;
justify-content: center;
width: 100%;
margin-right: auto;
margin-left: auto;
@media (min-width: 640px) {
width: 350px;
}
form {
display: grid;
gap: 0.5rem;
align-items: center;
max-width: 24rem;
}
}
</style> </style>