Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ coverage
dist
node_modules
package-lock.json
storage
11 changes: 3 additions & 8 deletions admin/Admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import * as actions from '~/admin/rpc/admin.ts'
import { state } from '~/admin/state.ts'
import { icon } from '~/lib/icon.ts'
import { Header } from '~/src/comp/Header.tsx'
import { Logout } from '~/src/comp/Logout.tsx'
import { whoami } from '~/src/rpc/auth.ts'
import { loginUserSession, maybeLogin, whoami } from '~/src/rpc/auth.ts'

const EDITABLE = new Set(['nick', 'email'])

Expand Down Expand Up @@ -83,11 +82,7 @@ function Table<T extends readonly [string, Record<string, unknown>]>({
}

export function Admin() {
if (!state.user) whoami().then(user => {
if (!user) location.href = '/'
else state.user = user
})

maybeLogin('/')
return <div>
<Header>
<div class="flex items-center gap-2">
Expand All @@ -97,7 +92,7 @@ export function Admin() {

<div class="flex items-center gap-2">
<span>{state.user?.nick}</span>
<Logout then={() => location.href = '/'} />

</div>
</Header>

Expand Down
16 changes: 15 additions & 1 deletion api/auth/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ export async function getUserByNick(nick: string) {
.executeTakeFirst()
}

export async function getUserByNickOrThrow(nick: string) {
return await db
.selectFrom('users')
.selectAll()
.where('nick', '=', nick)
.executeTakeFirstOrThrow()
}

export async function getUserByEmail(email: string) {
return await db
.selectFrom('users')
Expand All @@ -69,7 +77,13 @@ export async function loginUser(ctx: Context, nick: string) {
expires.setUTCFullYear(expires.getUTCFullYear() + 1)

const isAdmin = ADMINS.includes(nick)
const session: UserSession = { nick, expires, isAdmin }
const user = await getUserByNickOrThrow(nick)
const session: UserSession = {
nick,
expires,
isAdmin,
defaultProfile: user.defaultProfile ?? '(unset)'
}
await kv.set(sessionKey, session, {
expireIn: expires.getTime() - now.getTime()
})
Expand Down
1 change: 1 addition & 0 deletions api/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type UserSession = z.infer<typeof UserSession>
export const UserSession = z.object({
nick: z.string(),
expires: z.date(),
defaultProfile: z.string().optional(),
isAdmin: z.boolean().optional(),
})

Expand Down
7 changes: 5 additions & 2 deletions api/core/server.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import os from 'https://deno.land/x/os_paths@v7.4.0/src/mod.deno.ts'
import { parseArgs } from 'jsr:@std/cli/parse-args'
import * as path from 'jsr:@std/path'
import '~/api/chat/actions.ts'
import * as chat from '~/api/chat/routes.ts'
import { app } from '~/api/core/app.ts'
import { IS_DEV } from "~/api/core/constants.ts"
import { cors, files, logger, session, watcher } from "~/api/core/middleware.ts"
import '~/api/oauth/actions.ts'
import * as oauthCommon from '~/api/oauth/routes/common.ts'
import * as oauthGitHub from '~/api/oauth/routes/github.ts'
import * as rpc from '~/api/rpc/routes.ts'
import * as ws from '~/api/ws/routes.ts'

import '~/api/chat/actions.ts'
import '~/api/oauth/actions.ts'
import '~/api/profiles/actions.ts'
import '~/api/sounds/actions.ts'

const dist = 'dist'
const home = os.home() ?? '~'

Expand Down
59 changes: 59 additions & 0 deletions api/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ export interface ChannelUser {
nick: string;
}

export const Favorites = z.object({
profileNick: z.string(),
soundId: z.string(),
createdAt: z.coerce.date(),
})
export interface Favorites {
createdAt: Generated<Timestamp>;
profileNick: string;
soundId: string;
}

export const Messages = z.object({
id: z.string(),
channel: z.string(),
Expand All @@ -48,6 +59,46 @@ export interface Messages {
type: string;
}

export const Profiles = z.object({
ownerNick: z.string(),
nick: z.string(),
displayName: z.string(),
bio: z.string().nullish(),
avatar: z.string().nullish(),
banner: z.string().nullish(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
})
export interface Profiles {
avatar: string | null;
banner: string | null;
bio: string | null;
createdAt: Generated<Timestamp>;
displayName: string;
nick: string;
ownerNick: string;
updatedAt: Generated<Timestamp>;
}

export const Sounds = z.object({
id: z.string(),
ownerProfileNick: z.string(),
title: z.string(),
code: z.string(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
remixOf: z.string().nullish(),
})
export interface Sounds {
code: string;
createdAt: Generated<Timestamp>;
id: Generated<string>;
ownerProfileNick: string;
remixOf: string | null;
title: string;
updatedAt: Generated<Timestamp>;
}

export const Users = z.object({
nick: z.string(),
email: z.string(),
Expand All @@ -56,9 +107,11 @@ export const Users = z.object({
oauthGithub: z.boolean().nullish(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
defaultProfile: z.string().nullish(),
})
export interface Users {
createdAt: Generated<Timestamp>;
defaultProfile: string | null;
email: string;
emailVerified: Generated<boolean | null>;
nick: string;
Expand All @@ -70,12 +123,18 @@ export interface Users {
export const DB = z.object({
channels: Channels,
channelUser: ChannelUser,
favorites: Favorites,
messages: Messages,
profiles: Profiles,
sounds: Sounds,
users: Users,
})
export interface DB {
channels: Channels;
channelUser: ChannelUser;
favorites: Favorites;
messages: Messages;
profiles: Profiles;
sounds: Sounds;
users: Users;
}
78 changes: 78 additions & 0 deletions api/profiles/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { db } from '~/api/db.ts'
import { actions } from '~/api/rpc/routes.ts'
import { RouteError, type Context } from '~/api/core/router.ts'
import { getSession } from '~/api/core/sessions.ts'
import { Profiles } from '~/api/models.ts'
import type { ProfileCreate } from '~/api/profiles/types.ts'
import { loginUser } from '~/api/auth/actions.ts'

actions.post.createProfile = createProfile
export async function createProfile(ctx: Context, data: ProfileCreate) {
const session = getSession(ctx)
const { nick: ownerNick } = session

const result = await db
.insertInto('profiles')
.values({
ownerNick,
nick: data.nick,
displayName: data.displayName
})
.returning('nick')
.executeTakeFirstOrThrow()

if (data.isDefault || session.defaultProfile == null) {
await makeDefaultProfile(ctx, data.nick)
}

return result
}

actions.post.makeDefaultProfile = makeDefaultProfile
export async function makeDefaultProfile(ctx: Context, nick: string) {
const session = getSession(ctx)
const { nick: ownerNick } = session

await db
.updateTable('users')
.set({ defaultProfile: nick })
.where('nick', '=', ownerNick)
.returning('defaultProfile')
.executeTakeFirstOrThrow()

return loginUser(ctx, ownerNick)
}

actions.get.getProfile = getProfile
export async function getProfile(_ctx: Context, nick: string) {
return await db
.selectFrom('profiles')
.selectAll()
.where('nick', '=', nick)
.executeTakeFirstOrThrow()
}

actions.get.listProfilesForNick = listProfilesForNick
export async function listProfilesForNick(_ctx: Context, nick: string) {
return (await db
.selectFrom('profiles')
.selectAll()
.where('ownerNick', '=', nick)
.execute()
).map(entry => Profiles.parse(entry))
}

actions.post.deleteProfile = deleteProfile
export async function deleteProfile(ctx: Context, nick: string) {
const session = getSession(ctx)
const { nick: owner, defaultProfile } = session

if (nick === defaultProfile) throw new RouteError(400, 'Cannot delete default profile')

return await db
.deleteFrom('profiles')
.where('ownerNick', '=', owner)
.where('nick', '=', nick)
.returning('nick')
.executeTakeFirstOrThrow()
}
8 changes: 8 additions & 0 deletions api/profiles/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { z } from 'zod'

export type ProfileCreate = z.infer<typeof ProfileCreate>
export const ProfileCreate = z.object({
nick: z.string(),
displayName: z.string(),
isDefault: z.coerce.boolean(),
})
7 changes: 6 additions & 1 deletion api/rpc/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defer } from 'utils'
import { z } from 'zod'
import { RouteError, type Router } from '~/api/core/router.ts'
import { sessions } from '~/api/core/sessions.ts'
import { Kysely } from 'kysely'

const DEBUG = false

Expand Down Expand Up @@ -83,7 +84,11 @@ export function mount(app: Router) {
})
}
else {
return new Response(JSON.stringify({ error: error.message }), {
return new Response(JSON.stringify({
error: (error instanceof Error)
? (error as unknown as { detail?: string })?.detail ?? error.message
: 'Unknown error'
}), {
status: 500,
headers
})
Expand Down
Loading
Loading