This commit is contained in:
Shotaro Nakamura
2026-06-21 10:34:34 +09:00
parent 30639c5ae4
commit 17c5a1156e
4 changed files with 76 additions and 55 deletions
+3 -3
View File
@@ -20,14 +20,14 @@ export type StoredSbiPasskeySecret = {
deviceId?: string deviceId?: string
} }
const requireAuth: MiddlewareHandler<AppBindings> = async (c, next) => { const requireOwnerSession: MiddlewareHandler<AppBindings> = async (c, next) => {
if (!c.get('authenticated')) return c.json({ error: 'unauthorized' }, 401) if (c.get('auth').type !== 'session') return c.json({ error: 'unauthorized' }, 401)
await next() await next()
} }
export const createAdminRoutes = () => { export const createAdminRoutes = () => {
const app = new Hono<AppBindings>() const app = new Hono<AppBindings>()
app.use('*', requireAuth) app.use('*', requireOwnerSession)
app.get('/api-keys', async (c) => c.json({ apiKeys: await listApiKeys(c.get('db')) })) app.get('/api-keys', async (c) => c.json({ apiKeys: await listApiKeys(c.get('db')) }))
+5 -2
View File
@@ -1,5 +1,6 @@
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { Hono } from 'hono' import { Hono } from 'hono'
import type { Context } from 'hono'
import type { AppBindings } from '../context' import type { AppBindings } from '../context'
import { oauthClients } from '../db/schema' import { oauthClients } from '../db/schema'
import type { ApiKeySettings } from '../security/api-keys' import type { ApiKeySettings } from '../security/api-keys'
@@ -31,11 +32,13 @@ const redirectUriAllowed = (redirectUri: string, registeredUris: string[]) => {
}) })
} }
const isOwnerSession = (c: Context<AppBindings>) => c.get('auth').type === 'session'
export const createOAuthRoutes = () => { export const createOAuthRoutes = () => {
const app = new Hono<AppBindings>() const app = new Hono<AppBindings>()
app.get('/client/:id', async (c) => { app.get('/client/:id', async (c) => {
if (!c.get('authenticated')) return c.json({ error: 'unauthorized' }, 401) if (!isOwnerSession(c)) return c.json({ error: 'unauthorized' }, 401)
const [client] = await c const [client] = await c
.get('db') .get('db')
.select() .select()
@@ -46,7 +49,7 @@ export const createOAuthRoutes = () => {
}) })
app.post('/approve', async (c) => { app.post('/approve', async (c) => {
if (!c.get('authenticated')) return c.json({ error: 'unauthorized' }, 401) if (!isOwnerSession(c)) return c.json({ error: 'unauthorized' }, 401)
const body = await c.req.json<{ const body = await c.req.json<{
clientId?: string clientId?: string
redirectUri?: string redirectUri?: string
+53 -46
View File
@@ -1,36 +1,40 @@
export const ui = { export const ui = {
appShell: 'grid h-dvh overflow-hidden grid-cols-[7rem_minmax(0,1fr)] bg-[#101418] text-[#e3e3e9]', appShell:
sidebar: 'flex h-full min-h-0 flex-col items-center border-r border-[#2f3338] bg-[#191c20] py-4', 'grid h-dvh grid-rows-[minmax(0,1fr)_auto] overflow-hidden bg-[#101418] text-[#e3e3e9] lg:grid-cols-[7rem_minmax(0,1fr)] lg:grid-rows-none',
sidebar:
'order-2 flex h-20 min-h-0 items-center overflow-x-auto border-t border-[#2f3338] bg-[#191c20] px-2 py-2 lg:order-none lg:h-full lg:flex-col lg:overflow-visible lg:border-t-0 lg:border-r lg:px-0 lg:py-4',
brandMark: brandMark:
'grid h-14 w-14 place-items-center rounded-[18px] bg-[#a8c7fa] text-3xl font-black text-[#0b305f] shadow-lg shadow-black/20', 'hidden h-14 w-14 place-items-center rounded-[18px] bg-[#a8c7fa] text-3xl font-black text-[#0b305f] shadow-lg shadow-black/20 lg:grid',
navStack: 'mt-10 grid w-full gap-4 px-2', navStack:
'flex w-full min-w-max justify-around gap-2 lg:mt-10 lg:grid lg:min-w-0 lg:gap-4 lg:px-2',
navButton: navButton:
'grid min-h-16 place-items-center gap-1 rounded-[20px] bg-transparent p-1 text-xs font-semibold text-[#c3c7cf] transition hover:bg-[#22272e]', 'grid min-h-14 min-w-16 place-items-center gap-1 rounded-[20px] bg-transparent p-1 text-xs font-semibold text-[#c3c7cf] transition hover:bg-[#22272e] lg:min-h-16 lg:min-w-0',
navButtonActive: 'text-[#d3e3fd]', navButtonActive: 'text-[#d3e3fd]',
navIcon: 'grid h-8 w-16 place-items-center rounded-full text-[#c3c7cf]', navIcon: 'grid h-8 w-12 place-items-center rounded-full text-[#c3c7cf] lg:w-16',
navIconActive: 'bg-[#263141] text-[#d3e3fd]', navIconActive: 'bg-[#263141] text-[#d3e3fd]',
workspace: 'flex min-h-0 min-w-0 flex-col gap-7 overflow-y-auto px-8 pt-8 pb-8', workspace:
topbar: 'flex shrink-0 items-center justify-between gap-4', 'order-1 flex min-h-0 min-w-0 flex-col gap-5 overflow-y-auto px-4 pt-4 pb-5 sm:px-6 sm:pt-6 lg:order-none lg:gap-7 lg:px-8 lg:pt-8 lg:pb-8',
topbar: 'flex shrink-0 flex-wrap items-center justify-between gap-4',
tradeTopbar: 'hidden', tradeTopbar: 'hidden',
authPanel: 'grid justify-center pt-12', authPanel: 'grid justify-center pt-6 sm:pt-12',
panel: panel:
'grid content-start gap-4 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-6 shadow-lg shadow-black/15', 'grid content-start gap-4 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-4 shadow-lg shadow-black/15 sm:p-6',
loginPanel: 'w-[28rem]', loginPanel: 'w-full max-w-[28rem]',
panelHead: 'flex items-center justify-between gap-3', panelHead: 'flex flex-wrap items-center justify-between gap-3',
eyebrow: 'mb-1 text-xs font-black uppercase text-[#9aa0a9]', eyebrow: 'mb-1 text-xs font-black uppercase text-[#9aa0a9]',
dashboardGrid: dashboardGrid:
'grid min-h-0 flex-1 grid-cols-[minmax(0,1.25fr)_minmax(20rem,0.75fr)] grid-rows-[auto_minmax(0,1fr)] items-stretch gap-6 overflow-hidden', 'grid min-h-0 flex-1 grid-cols-1 items-stretch gap-5 lg:grid-cols-[minmax(0,1.25fr)_minmax(20rem,0.75fr)] lg:grid-rows-[auto_minmax(0,1fr)] lg:gap-6 lg:overflow-hidden',
metricPanel: metricPanel:
'grid min-h-40 content-center gap-3 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-7 shadow-lg shadow-black/15', 'grid min-h-36 content-center gap-3 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-5 shadow-lg shadow-black/15 sm:min-h-40 sm:p-7',
assetOverviewPanel: assetOverviewPanel:
'grid min-h-40 grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] items-center gap-x-7 gap-y-4 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-7 shadow-lg shadow-black/15', 'grid min-h-40 grid-cols-1 items-center gap-x-7 gap-y-5 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-5 shadow-lg shadow-black/15 sm:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] sm:p-7',
assetOverviewHead: 'grid gap-2', assetOverviewHead: 'grid gap-2',
assetOverviewSubtext: 'text-sm font-semibold text-[#c3c7cf]', assetOverviewSubtext: 'text-sm font-semibold text-[#c3c7cf]',
assetBreakdownPanel: 'grid gap-3', assetBreakdownPanel: 'grid gap-3',
assetBreakdownTitle: 'text-xs font-extrabold text-[#9aa0a9]', assetBreakdownTitle: 'text-xs font-extrabold text-[#9aa0a9]',
metricLabel: 'text-xs font-extrabold text-[#9aa0a9]', metricLabel: 'text-xs font-extrabold text-[#9aa0a9]',
metricValue: 'text-3xl font-black', metricValue: 'break-words text-2xl font-black sm:text-3xl',
positive: 'text-[#40dba2]', positive: 'text-[#40dba2]',
negative: 'text-[#ffb4ab]', negative: 'text-[#ffb4ab]',
miniProgress: 'block h-2 w-full overflow-hidden rounded-full bg-[#33383f]', miniProgress: 'block h-2 w-full overflow-hidden rounded-full bg-[#33383f]',
@@ -48,48 +52,51 @@ export const ui = {
assetBreakdownAmount: 'text-sm font-black text-[#e3e3e9]', assetBreakdownAmount: 'text-sm font-black text-[#e3e3e9]',
assetBreakdownRatio: 'text-xs font-semibold text-[#8f949d]', assetBreakdownRatio: 'text-xs font-semibold text-[#8f949d]',
holdingsPanel: holdingsPanel:
'flex min-h-0 flex-col gap-4 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-7 shadow-lg shadow-black/15', 'flex min-h-0 flex-col gap-4 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-5 shadow-lg shadow-black/15 sm:p-7',
holdingsBody: 'flex min-h-0 flex-1 flex-col overflow-hidden', holdingsBody: 'flex min-h-0 flex-1 flex-col overflow-visible lg:overflow-hidden',
holdingsRows: 'grid min-h-0 flex-1 content-start overflow-y-auto', holdingsRows: 'grid min-h-0 flex-1 content-start overflow-y-auto',
holdingsHead: holdingsHead:
'grid grid-cols-[1.7fr_0.8fr_0.7fr_1fr_1fr] items-center gap-4 border-b border-[#33383f] py-3 text-xs font-extrabold text-[#8f949d]', 'hidden grid-cols-[1.7fr_0.8fr_0.7fr_1fr_1fr] items-center gap-4 border-b border-[#33383f] py-3 text-xs font-extrabold text-[#8f949d] md:grid',
holdingRow: holdingRow:
'grid min-h-16 grid-cols-[1.7fr_0.8fr_0.7fr_1fr_1fr] items-center gap-4 rounded-2xl bg-transparent px-3 py-3 text-left text-[#e3e3e9] transition hover:bg-[#242930]', 'grid min-h-16 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-2xl bg-[#111418] px-3 py-3 text-left text-[#e3e3e9] transition hover:bg-[#242930] md:grid-cols-[1.7fr_0.8fr_0.7fr_1fr_1fr] md:gap-4 md:bg-transparent',
typePill: 'w-fit rounded-full bg-[#263141] px-3 py-1 text-xs text-[#d3e3fd]', typePill: 'w-fit rounded-full bg-[#263141] px-3 py-1 text-xs text-[#d3e3fd]',
muted: 'text-[#8f949d]', muted: 'text-[#8f949d]',
portfolioHistory: portfolioHistory:
'flex min-h-0 flex-col gap-4 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-7 shadow-lg shadow-black/15', 'flex min-h-0 flex-col gap-4 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-5 shadow-lg shadow-black/15 sm:p-7',
historyList: 'flex min-h-0 flex-1 flex-col overflow-hidden', historyList: 'flex min-h-0 flex-1 flex-col overflow-visible lg:overflow-hidden',
historyRows: 'grid min-h-0 flex-1 content-start gap-7 overflow-y-auto', historyRows: 'grid min-h-0 flex-1 content-start gap-5 overflow-y-auto lg:gap-7',
emptyState: 'flex flex-1 items-center justify-center py-8 text-center', emptyState: 'flex flex-1 items-center justify-center py-8 text-center',
miniOrder: 'grid grid-cols-[minmax(0,1fr)_auto] gap-3', miniOrder: 'grid grid-cols-[minmax(0,1fr)_auto] gap-3',
tradeLayout: 'trade-layout grid w-full min-h-[calc(100dvh-6rem)] grid-cols-5 items-stretch gap-5', tradeLayout:
'trade-layout grid w-full min-w-0 grid-cols-1 items-stretch gap-5 lg:min-h-[calc(100dvh-6rem)] lg:grid-cols-5',
watchlist: watchlist:
'order-3 pl-2 col-span-1 grid h-full min-h-[calc(100dvh-6rem)] content-start overflow-y-auto border-l border-[#30343a] bg-[#101418] shadow-none', 'order-3 grid min-w-0 content-start gap-2 overflow-visible border-t border-[#30343a] bg-[#101418] pt-4 shadow-none lg:col-span-1 lg:h-full lg:min-h-[calc(100dvh-6rem)] lg:overflow-y-auto lg:border-t-0 lg:border-l lg:pt-0 lg:pl-2',
watchSearch: watchSearch:
'm-3 inline-flex min-h-11 items-center gap-2 rounded-full bg-[#111418] px-4 text-left font-medium text-[#9aa0a9] outline outline-1 outline-[#33383f] transition hover:bg-[#22272e] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#d3e3fd]', 'm-3 inline-flex min-h-11 items-center gap-2 rounded-full bg-[#111418] px-4 text-left font-medium text-[#9aa0a9] outline outline-1 outline-[#33383f] transition hover:bg-[#22272e] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#d3e3fd]',
watchRow: watchRow:
'grid min-h-18 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-lg border-l-2 border-transparent px-4 py-3 text-left text-[#e3e3e9] transition hover:bg-[#242930] focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[#d3e3fd]', 'grid min-h-18 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-lg border-l-2 border-transparent px-4 py-3 text-left text-[#e3e3e9] transition hover:bg-[#242930] focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[#d3e3fd]',
watchRowActive: 'bg-gray-900', watchRowActive: 'bg-gray-900',
centerStack: 'order-1 col-span-3 flex min-h-0 h-full flex-col gap-5', centerStack: 'order-1 col-span-3 flex min-h-0 h-full flex-col gap-5',
stockMetadataPanel: 'grid min-h-0 gap-4 rounded-[24px] border-0 bg-[#101418] p-6 shadow-none', stockMetadataPanel:
stockBodyGrid: 'flex-1 min-h-0 grid grid-rows-3 gap-5', 'grid min-h-0 gap-4 rounded-[24px] border-0 bg-[#101418] p-0 shadow-none sm:p-6',
stockBodyGrid: 'grid min-h-0 flex-1 gap-5 lg:grid-rows-3',
tradeSeparator: 'order-3 hidden', tradeSeparator: 'order-3 hidden',
stockPanel: stockPanel:
'grid min-h-0 h-full grid-rows-[auto_auto_1fr] gap-4 rounded-[24px] border-0 bg-[#101418] p-6 shadow-none', 'grid min-h-0 h-full grid-rows-[auto_auto_1fr] gap-4 rounded-[24px] border-0 bg-[#101418] p-0 shadow-none sm:p-6',
stockPanelSpan: 'row-span-2', stockPanelSpan: 'lg:row-span-2',
stockTitle: 'flex items-start justify-between gap-3', stockTitle: 'flex flex-wrap items-start justify-between gap-3',
stockHoldingNote: stockHoldingNote:
'inline-flex w-fit items-center rounded-full border border-[#2f343b] bg-[#111418] px-4 py-2 text-sm font-semibold text-[#c3c7cf]', 'inline-flex w-fit items-center rounded-full border border-[#2f343b] bg-[#111418] px-4 py-2 text-sm font-semibold text-[#c3c7cf]',
quoteBox: 'grid justify-items-end gap-1', quoteBox: 'grid justify-items-start gap-1 sm:justify-items-end',
periodTabs: 'flex flex-wrap justify-end gap-2', periodTabs: 'flex flex-wrap justify-start gap-2 sm:justify-end',
periodButton: 'min-h-7 rounded-full bg-transparent px-3 text-xs font-bold text-[#8f949d]', periodButton: 'min-h-7 rounded-full bg-transparent px-3 text-xs font-bold text-[#8f949d]',
periodButtonActive: 'bg-[#d3e3fd] text-[#102033]', periodButtonActive: 'bg-[#d3e3fd] text-[#102033]',
chartActions: 'flex items-center justify-between gap-4', chartActions: 'flex flex-wrap items-center justify-between gap-4',
smallTabs: 'grid grid-cols-2 gap-1 rounded-full bg-[#111418] p-1', smallTabs: 'grid grid-cols-2 gap-1 rounded-full bg-[#111418] p-1',
smallTab: 'min-h-8 rounded-full bg-transparent px-3 text-xs font-bold text-[#9aa0a9]', smallTab: 'min-h-8 rounded-full bg-transparent px-3 text-xs font-bold text-[#9aa0a9]',
smallTabActive: 'bg-[#263141] text-[#d3e3fd]', smallTabActive: 'bg-[#263141] text-[#d3e3fd]',
chartBox: 'mt-1 min-h-0 h-full min-h-[14rem] overflow-hidden rounded-[20px] bg-[#111418]', chartBox:
'mt-1 h-72 min-h-0 overflow-hidden rounded-[20px] bg-[#111418] lg:h-full lg:min-h-[14rem]',
chartLine: 'fill-none stroke-[#40dba2] stroke-[3]', chartLine: 'fill-none stroke-[#40dba2] stroke-[3]',
boxplot: 'grid min-h-36 content-center gap-5 rounded-[24px] bg-[#111418] p-7', boxplot: 'grid min-h-36 content-center gap-5 rounded-[24px] bg-[#111418] p-7',
boxplotScale: 'flex justify-between text-xs text-[#8f949d]', boxplotScale: 'flex justify-between text-xs text-[#8f949d]',
@@ -98,7 +105,7 @@ export const ui = {
whisker: 'absolute top-9 h-3 border-x-2 border-[#c3c7cf]', whisker: 'absolute top-9 h-3 border-x-2 border-[#c3c7cf]',
box: 'absolute top-6 h-9 rounded-xl border-2 border-[#a8c7fa] bg-[#a8c7fa]/25', box: 'absolute top-6 h-9 rounded-xl border-2 border-[#a8c7fa] bg-[#a8c7fa]/25',
median: 'absolute top-4 h-12 border-l-4 border-[#fdd663]', median: 'absolute top-4 h-12 border-l-4 border-[#fdd663]',
infoTabPanel: 'grid gap-0 rounded-[24px] border-0 bg-[#101418] p-6 shadow-none', infoTabPanel: 'grid gap-0 rounded-[24px] border-0 bg-[#101418] p-0 shadow-none sm:p-6',
infoTabList: 'flex gap-6 border-b border-[#33383f]', infoTabList: 'flex gap-6 border-b border-[#33383f]',
infoTabButton: infoTabButton:
'relative min-h-10 px-1 text-sm font-black text-[#8f949d] transition hover:text-[#d3e3fd] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#d3e3fd]', 'relative min-h-10 px-1 text-sm font-black text-[#8f949d] transition hover:text-[#d3e3fd] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#d3e3fd]',
@@ -106,12 +113,12 @@ export const ui = {
infoTabIndicator: 'absolute inset-x-0 bottom-[-1px] block h-0.5 rounded-full bg-[#d3e3fd]', infoTabIndicator: 'absolute inset-x-0 bottom-[-1px] block h-0.5 rounded-full bg-[#d3e3fd]',
infoTabBody: 'overflow-hidden pt-4', infoTabBody: 'overflow-hidden pt-4',
infoTabBodyInner: 'relative min-h-[9.5rem]', infoTabBodyInner: 'relative min-h-[9.5rem]',
detailGrid: 'grid grid-cols-3 gap-3', detailGrid: 'grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3',
detailItem: 'grid min-h-16 gap-1 rounded-[14px] bg-[#111418] p-3', detailItem: 'grid min-h-16 gap-1 rounded-[14px] bg-[#111418] p-3',
holdingEmpty: holdingEmpty:
'rounded-[16px] border border-[#2f343b] bg-[#111418] px-4 py-3 text-sm font-semibold text-[#8f949d]', 'rounded-[16px] border border-[#2f343b] bg-[#111418] px-4 py-3 text-sm font-semibold text-[#8f949d]',
ticketPanel: ticketPanel:
'order-2 col-span-1 flex h-full min-h-0 flex-col gap-4 overflow-hidden border-0 bg-[#101418] p-6 shadow-none', 'order-2 col-span-1 flex h-full min-h-0 flex-col gap-4 overflow-visible border-0 bg-[#101418] p-0 shadow-none sm:p-6 lg:overflow-hidden',
ticketTop: 'shrink-0', ticketTop: 'shrink-0',
ticketScroll: 'min-h-0 flex-1 overflow-y-auto pr-1', ticketScroll: 'min-h-0 flex-1 overflow-y-auto pr-1',
ticketScrollInner: 'grid content-start gap-3', ticketScrollInner: 'grid content-start gap-3',
@@ -134,7 +141,7 @@ export const ui = {
input: input:
'min-h-12 w-full rounded-[16px] border border-[#4a5058] bg-[#111418] px-4 text-[#e3e3e9] outline-none transition placeholder:text-[#747982] focus:border-[#a8c7fa]', 'min-h-12 w-full rounded-[16px] border border-[#4a5058] bg-[#111418] px-4 text-[#e3e3e9] outline-none transition placeholder:text-[#747982] focus:border-[#a8c7fa]',
estimateSummary: estimateSummary:
'flex items-center justify-between rounded-[18px] border border-[#2f343b] bg-[#111418] px-5 py-4', 'flex flex-wrap items-center justify-between gap-2 rounded-[18px] border border-[#2f343b] bg-[#111418] px-5 py-4',
holdingNote: holdingNote:
'rounded-[16px] border border-[#2f343b] bg-[#111418] px-4 py-3 text-sm font-semibold text-[#c3c7cf]', 'rounded-[16px] border border-[#2f343b] bg-[#111418] px-4 py-3 text-sm font-semibold text-[#c3c7cf]',
actions: 'grid gap-3', actions: 'grid gap-3',
@@ -146,25 +153,25 @@ export const ui = {
'inline-flex min-h-10 items-center justify-center gap-2 rounded-full border border-[#4a5058] bg-transparent px-5 font-extrabold text-[#d3e3fd] transition hover:bg-[#263141]', 'inline-flex min-h-10 items-center justify-center gap-2 rounded-full border border-[#4a5058] bg-transparent px-5 font-extrabold text-[#d3e3fd] transition hover:bg-[#263141]',
list: 'grid gap-2', list: 'grid gap-2',
orderRow: orderRow:
'grid grid-cols-[minmax(180px,1fr)_auto_auto_auto_auto_auto] items-center gap-3 rounded-[20px] bg-[#111418] p-4', 'grid grid-cols-2 items-center gap-3 rounded-[20px] bg-[#111418] p-4 md:grid-cols-[minmax(180px,1fr)_auto_auto_auto_auto_auto]',
statusBadge: 'w-fit rounded-full bg-[#263141] px-3 py-1 text-xs font-black text-[#d3e3fd]', statusBadge: 'w-fit rounded-full bg-[#263141] px-3 py-1 text-xs font-black text-[#d3e3fd]',
pendingBadge: 'bg-[#4a3720] text-[#ffddb3]', pendingBadge: 'bg-[#4a3720] text-[#ffddb3]',
apiLayout: 'grid grid-cols-[minmax(0,1fr)_minmax(20rem,0.9fr)] gap-7', apiLayout: 'grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_minmax(20rem,0.9fr)] lg:gap-7',
settingsLayout: 'grid gap-7', settingsLayout: 'grid gap-7',
row: 'grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-[20px] bg-[#111418] p-4', row: 'grid grid-cols-1 items-center gap-3 rounded-[20px] bg-[#111418] p-4 sm:grid-cols-[minmax(0,1fr)_auto]',
rowActions: 'flex gap-2', rowActions: 'flex gap-2',
keyRow: 'grid gap-3 rounded-[24px] bg-[#111418] p-4', keyRow: 'grid gap-3 rounded-[24px] bg-[#111418] p-4',
profileRow: profileRow:
'grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 rounded-[20px] bg-[#111418] p-4', 'grid grid-cols-[auto_minmax(0,1fr)] items-center gap-3 rounded-[20px] bg-[#111418] p-4 sm:grid-cols-[auto_minmax(0,1fr)_auto]',
searchOverlay: 'fixed inset-0 z-20 bg-[#101418]/75 backdrop-blur-sm', searchOverlay: 'fixed inset-0 z-20 bg-[#101418]/75 backdrop-blur-sm',
searchSheet: searchSheet:
'mx-auto mt-4 grid max-h-[85vh] w-[48rem] gap-3 overflow-auto rounded-[28px] border border-[#30343a] bg-[#1b1f24]/85 p-4 shadow-2xl shadow-black/35', 'mx-auto mt-2 grid max-h-[calc(100dvh-1rem)] w-[calc(100vw-1rem)] max-w-[48rem] gap-3 overflow-auto rounded-[28px] border border-[#30343a] bg-[#1b1f24]/85 p-3 shadow-2xl shadow-black/35 sm:mt-4 sm:max-h-[85vh] sm:w-[calc(100vw-2rem)] sm:p-4 md:w-[48rem]',
searchInputRow: 'grid grid-cols-[minmax(0,1fr)_auto] gap-3', searchInputRow: 'grid grid-cols-1 gap-3 sm:grid-cols-[minmax(0,1fr)_auto]',
filterRow: 'grid grid-cols-2 gap-3', filterRow: 'grid grid-cols-1 gap-3 sm:grid-cols-2',
searchResults: 'grid gap-2', searchResults: 'grid gap-2',
searchLoading: 'flex min-h-32 flex-col items-center justify-center gap-3 py-8 text-[#9aa0a8]', searchLoading: 'flex min-h-32 flex-col items-center justify-center gap-3 py-8 text-[#9aa0a8]',
searchResult: searchResult:
'flex min-h-16 items-center justify-between gap-4 rounded-[20px] border border-transparent bg-[#111418] p-4 text-left text-[#e3e3e9] transition hover:bg-[#242930] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#d3e3fd]', 'flex min-h-16 flex-wrap items-center justify-between gap-4 rounded-[20px] border border-transparent bg-[#111418] p-4 text-left text-[#e3e3e9] transition hover:bg-[#242930] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#d3e3fd]',
searchResultActive: 'border-l-4 border-[#d3e3fd] bg-[#242930]', searchResultActive: 'border-l-4 border-[#d3e3fd] bg-[#242930]',
searchResultFocus: searchResultFocus:
'bg-[#2a323a] outline outline-2 outline-[#d3e3fd] outline-offset-[-2px] shadow-[0_0_0_1px_rgba(211,227,253,0.35)]', 'bg-[#2a323a] outline outline-2 outline-[#d3e3fd] outline-offset-[-2px] shadow-[0_0_0_1px_rgba(211,227,253,0.35)]',
+15 -4
View File
@@ -1,5 +1,5 @@
import { existsSync } from 'node:fs' import { existsSync } from 'node:fs'
import { extname, join } from 'node:path' import { extname, isAbsolute, join, relative, resolve } from 'node:path'
import { Hono } from 'hono' import { Hono } from 'hono'
import { createServerApp } from '@repo/csbie-server/app' import { createServerApp } from '@repo/csbie-server/app'
import { loadConfig } from '@repo/csbie-server/config' import { loadConfig } from '@repo/csbie-server/config'
@@ -9,7 +9,7 @@ const config = loadConfig()
const db = createDb(config.databasePath) const db = createDb(config.databasePath)
const api = createServerApp(db, config) const api = createServerApp(db, config)
const app = new Hono() const app = new Hono()
const uiDist = join(import.meta.dir, '../../csbie-ui/dist') const uiDist = resolve(import.meta.dir, '../../csbie-ui/dist')
const contentTypes: Record<string, string> = { const contentTypes: Record<string, string> = {
'.css': 'text/css; charset=utf-8', '.css': 'text/css; charset=utf-8',
@@ -23,11 +23,22 @@ const contentTypes: Record<string, string> = {
app.route('/api', api.app) app.route('/api', api.app)
app.route('/', api.app) app.route('/', api.app)
const isInsideUiDist = (path: string) => {
const pathFromUiDist = relative(uiDist, path)
return pathFromUiDist === '' || (!pathFromUiDist.startsWith('..') && !isAbsolute(pathFromUiDist))
}
const serveUi = async (request: Request) => { const serveUi = async (request: Request) => {
const url = new URL(request.url) const url = new URL(request.url)
const pathname = decodeURIComponent(url.pathname) let pathname: string
try {
pathname = decodeURIComponent(url.pathname)
} catch {
return new Response('Bad Request', { status: 400 })
}
const relativePath = pathname === '/' ? 'index.html' : pathname.slice(1) const relativePath = pathname === '/' ? 'index.html' : pathname.slice(1)
const filePath = join(uiDist, relativePath) const filePath = resolve(uiDist, relativePath)
if (!isInsideUiDist(filePath)) return new Response('Not Found', { status: 404 })
const resolvedPath = existsSync(filePath) ? filePath : join(uiDist, 'index.html') const resolvedPath = existsSync(filePath) ? filePath : join(uiDist, 'index.html')
const file = Bun.file(resolvedPath) const file = Bun.file(resolvedPath)
const headers = new Headers() const headers = new Headers()