WIP: auth public
This commit is contained in:
parent
794d81438d
commit
e58e589f8e
@ -2,7 +2,7 @@ import { h, type FunctionComponent } from 'preact'
|
||||
import { useState } from 'preact/hooks'
|
||||
import { Router } from 'preact-router'
|
||||
|
||||
import CurrentUserContext from '../contexts/current_user.ts'
|
||||
import CurrentUserContext from '../../shared/contexts/current_user.ts'
|
||||
import { NotificationsProvider } from '../contexts/notifications.tsx'
|
||||
import routes from '../routes.ts'
|
||||
|
||||
@ -22,7 +22,7 @@ const App: FunctionComponent<{ state: ANY }> = ({ state }) => {
|
||||
<CurrentUserContext.Provider value={{ user, setUser }}>
|
||||
<div className={s.base}>
|
||||
<a href='/admin' className={s.logo}>
|
||||
Carson Admin
|
||||
BRF Admin
|
||||
</a>
|
||||
|
||||
<header className={s.header}>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { h, type FunctionComponent } from 'preact'
|
||||
import cn from 'classnames'
|
||||
|
||||
import { useCurrentUser } from '../contexts/current_user.ts'
|
||||
import { useCurrentUser } from '../../shared/contexts/current_user.ts'
|
||||
import s from './current_user.module.scss'
|
||||
|
||||
const CurrentUser: FunctionComponent<{ className?: string }> = ({ className }) => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { h, type FunctionComponent, type TargetedSubmitEvent } from 'preact'
|
||||
import type { FetchError } from 'rek'
|
||||
import { useCallback } from 'preact/hooks'
|
||||
import { useCurrentUser } from '../contexts/current_user.ts'
|
||||
import { useCurrentUser } from '../../shared/contexts/current_user.ts'
|
||||
import rek from 'rek'
|
||||
|
||||
import useRequestState from '../../shared/hooks/use_request_state.ts'
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import { h } from 'preact'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { route } from 'preact-router'
|
||||
import { useCurrentUser } from '../contexts/current_user.ts'
|
||||
import { useCurrentUser } from '../../shared/contexts/current_user.ts'
|
||||
|
||||
/** @type {import('preact').FunctionComponent<{ auth: boolean, path: string, component: () => any, loadComponent: boolean}} Page */
|
||||
const Route = ({ auth, path, component, loadComponent = true }) => {
|
||||
|
||||
@ -3,6 +3,6 @@ import './styles/main.scss'
|
||||
import { h, hydrate } from 'preact'
|
||||
import App from './components/app.tsx'
|
||||
|
||||
const STATE = typeof __STATE__ !== 'undefined' ? __STATE__ : undefined
|
||||
const state = typeof __STATE__ === 'undefined' ? { user: null } : __STATE__
|
||||
|
||||
hydrate(h(App, STATE), document.body)
|
||||
hydrate(h(App, { state }), document.body)
|
||||
|
||||
90
client/public/components/account_page.tsx
Normal file
90
client/public/components/account_page.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import { h, type FunctionComponent } from 'preact'
|
||||
import { useEffect, useRef } from 'preact/hooks'
|
||||
import * as d3 from 'd3'
|
||||
import rek from 'rek'
|
||||
import Head from './head.ts'
|
||||
import usePromise from '../../shared/hooks/use_promise.ts'
|
||||
|
||||
type AccountPageProps = {
|
||||
number: number
|
||||
}
|
||||
|
||||
function empty(element: HTMLElement) {
|
||||
while (element.lastElementChild) {
|
||||
element.removeChild(element.lastElementChild)
|
||||
}
|
||||
}
|
||||
|
||||
const AccountPage: FunctionComponent<AccountPageProps> = ({ number }) => {
|
||||
const graphRef = useRef<HTMLDivElement>(null)
|
||||
const sums = usePromise<{ month: string; totalAmount: number }[]>(() => rek(`/api/accounts/month-sum/${number}`))
|
||||
|
||||
useEffect(() => {
|
||||
empty(graphRef.current!)
|
||||
|
||||
const width = 1000
|
||||
const height = 1000
|
||||
const margin = { left: 50, bottom: 40 }
|
||||
const stageWidth = 1000 - margin.left
|
||||
const stageHeight = 1000 - margin.bottom
|
||||
|
||||
const svg = d3
|
||||
.select(graphRef.current)
|
||||
.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left}, 0)`)
|
||||
|
||||
const x = d3.scaleBand(
|
||||
sums.map(({ month }) => month),
|
||||
[0, stageWidth],
|
||||
)
|
||||
|
||||
svg.append('g').attr('transform', `translate(0, ${stageHeight})`).call(d3.axisBottom(x))
|
||||
|
||||
const y = d3.scaleLinear([0, d3.max(sums, (d) => Math.abs(d.totalAmount))], [stageHeight, 0])
|
||||
|
||||
svg.append('g').call(d3.axisLeft(y))
|
||||
svg
|
||||
.append('path')
|
||||
.datum(sums)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'steelblue')
|
||||
.attr('stroke-width', 1.5)
|
||||
.attr(
|
||||
'd',
|
||||
d3
|
||||
.line()
|
||||
.x((d) => x(d.month))
|
||||
.y((d) => y(Math.abs(d.totalAmount))),
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Head>
|
||||
<title> : Konto</title>
|
||||
</Head>
|
||||
|
||||
<h1>Konto {number}</h1>
|
||||
<div ref={graphRef} />
|
||||
<table className='grid'>
|
||||
<thead>
|
||||
<th>Månad</th>
|
||||
<th>Summa</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sums.map((sum) => (
|
||||
<tr>
|
||||
<td>{sum.month}</td>
|
||||
<td>{sum.totalAmount}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountPage
|
||||
@ -23,7 +23,9 @@ const AccountsPage: FunctionComponent = () => {
|
||||
<tbody>
|
||||
{accounts.map((account) => (
|
||||
<tr>
|
||||
<td>{account.number}</td>
|
||||
<td>
|
||||
<a href={`/accounts/${account.number}`}>{account.number}</a>
|
||||
</td>
|
||||
<td>{account.description}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@ -1,19 +1,22 @@
|
||||
import { h, type FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef } from 'preact/hooks'
|
||||
import { LocationProvider, Route, Router } from 'preact-iso'
|
||||
import { LocationProvider, Router } from 'preact-iso'
|
||||
import { get } from 'lowline'
|
||||
import { AuthProvider } from '../../shared/contexts/auth.tsx'
|
||||
import throttle from '../../shared/utils/throttle.ts'
|
||||
import Head from './head.ts'
|
||||
import Footer from './footer.tsx'
|
||||
import Header from './header.tsx'
|
||||
import ErrorPage from './error_page.tsx'
|
||||
import { AuthRoute, Refresh } from './route.tsx'
|
||||
import routes from '../routes.ts'
|
||||
import throttle from '../../shared/utils/throttle.ts'
|
||||
|
||||
import s from './app.module.scss'
|
||||
|
||||
type Props = {
|
||||
error?: Error
|
||||
title?: string
|
||||
state: ANY
|
||||
}
|
||||
|
||||
const scroll = () => {
|
||||
@ -22,7 +25,7 @@ const scroll = () => {
|
||||
window.scrollTo(0, offset || 0)
|
||||
}
|
||||
|
||||
const App: FunctionComponent<Props> = ({ error, title }) => {
|
||||
const App: FunctionComponent<Props> = ({ error, title, state }) => {
|
||||
const loadRef = useRef<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
@ -61,9 +64,10 @@ const App: FunctionComponent<Props> = ({ error, title }) => {
|
||||
|
||||
return (
|
||||
<LocationProvider>
|
||||
<AuthProvider user={state.user}>
|
||||
<div id='app' className={s.base}>
|
||||
<Head>
|
||||
<title>{title || 'Untitled'}</title>
|
||||
<title>{title || 'BRF'}</title>
|
||||
</Head>
|
||||
|
||||
<Header routes={routes} />
|
||||
@ -74,14 +78,16 @@ const App: FunctionComponent<Props> = ({ error, title }) => {
|
||||
) : (
|
||||
<Router onLoadStart={onLoadStart} onLoadEnd={onLoadEnd} onRouteChange={onRouteChange}>
|
||||
{routes.map((route) => (
|
||||
<Route key={route.path} path={route.path} component={route.component} />
|
||||
<AuthRoute key={route.path} {...route} />
|
||||
))}
|
||||
<Refresh path='/admin' />
|
||||
</Router>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</AuthProvider>
|
||||
</LocationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
5
client/public/components/current_user.module.scss
Normal file
5
client/public/components/current_user.module.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: end;
|
||||
}
|
||||
38
client/public/components/current_user.tsx
Normal file
38
client/public/components/current_user.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { h, type FunctionComponent } from 'preact'
|
||||
import { Show } from '@preact/signals/utils'
|
||||
import { useComputed } from '@preact/signals'
|
||||
import cn from 'classnames'
|
||||
|
||||
import { useAuth } from '../../shared/contexts/auth.tsx'
|
||||
import s from './current_user.module.scss'
|
||||
|
||||
const CurrentUser: FunctionComponent<{ className?: string }> = ({ className }) => {
|
||||
const { user } = useAuth()
|
||||
const noUser = useComputed(() => !user.value)
|
||||
|
||||
return (
|
||||
<div className={cn(s.base, className)}>
|
||||
<Show when={user}>
|
||||
<div className={s.email}>{user.value?.email}</div>
|
||||
|
||||
<div className={s.links}>
|
||||
<a href='/auth/logout' target='_parent'>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={noUser}>
|
||||
<div className={cn(s.base, className)}>
|
||||
<p>You are not logged in</p>
|
||||
|
||||
<div className={s.links}>
|
||||
<a href='/login'>Login</a>
|
||||
<a href='/register'>Register</a>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CurrentUser
|
||||
@ -1,6 +1,14 @@
|
||||
@use '../../shared/styles/utils';
|
||||
|
||||
.base {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
grid-template-rows: auto auto;
|
||||
}
|
||||
|
||||
.nav {
|
||||
grid-area: 2 / 1 / 3 / 2;
|
||||
|
||||
> ul {
|
||||
@include utils.wipe-list();
|
||||
|
||||
@ -11,6 +19,16 @@
|
||||
display: block;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
> a {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.currentUser {
|
||||
grid-area: 1 / 2 / 3 / 3;
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { h, type FunctionComponent } from 'preact'
|
||||
import CurrentUser from './current_user.tsx'
|
||||
import s from './header.module.scss'
|
||||
|
||||
import type { Route } from '../../../shared/types.ts'
|
||||
|
||||
const Header: FunctionComponent<{ routes: Route[] }> = ({ routes }) => (
|
||||
<header>
|
||||
<header className={s.base}>
|
||||
<h1>BRF Tegeltrasten</h1>
|
||||
<nav className={s.nav}>
|
||||
<ul>
|
||||
@ -17,6 +18,7 @@ const Header: FunctionComponent<{ routes: Route[] }> = ({ routes }) => (
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
<CurrentUser className={s.currentUser} />
|
||||
</header>
|
||||
)
|
||||
|
||||
|
||||
69
client/public/components/input.module.scss
Normal file
69
client/public/components/input.module.scss
Normal file
@ -0,0 +1,69 @@
|
||||
@use 'sass:color';
|
||||
|
||||
.base {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:not(:last-child):not(.noMargin) {
|
||||
margin-bottom: var(--gutter);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
order: 1;
|
||||
font-weight: bold;
|
||||
margin-bottom: var(--form-label-margin);
|
||||
|
||||
&:has(input:invalid) {
|
||||
color: var(--form-invalid-color);
|
||||
}
|
||||
|
||||
&:has(input:valid) {
|
||||
color: var(--form-valid-color);
|
||||
}
|
||||
}
|
||||
|
||||
.element {
|
||||
order: 2;
|
||||
width: 100%;
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--form-border-color);
|
||||
padding: var(--form-element-padding);
|
||||
font-family: var(--body-font-family);
|
||||
background: white;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-blue);
|
||||
// box-shadow: 0 0 5px 0px color.scale(var(--color-blue), $alpha: -20%);
|
||||
}
|
||||
|
||||
.touched &:invalid {
|
||||
border-color: var(--form-invalid-color);
|
||||
|
||||
~ .label {
|
||||
color: var(--form-invalid-color);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
// box-shadow: 0 0 5px 0px color.scale(var(--form-invalid-color), $alpha: -20%);
|
||||
}
|
||||
}
|
||||
|
||||
.touched &:valid {
|
||||
border-color: var(--form-valid-color);
|
||||
|
||||
~ .label {
|
||||
color: var(--form-valid-color);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
// box-shadow: 0 0 5px 0px color.scale(var(--form-valid-color), $alpha: -20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.errorLabel {
|
||||
color: var(--form-invalid-color);
|
||||
margin-top: var(--form-label-margin);
|
||||
}
|
||||
5
client/public/components/input.tsx
Normal file
5
client/public/components/input.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import inputFactory from '../../shared/components/input_factory.tsx'
|
||||
|
||||
import s from './input.module.scss'
|
||||
|
||||
export default inputFactory(s)
|
||||
30
client/public/components/login_form.module.scss
Normal file
30
client/public/components/login_form.module.scss
Normal file
@ -0,0 +1,30 @@
|
||||
.root {
|
||||
max-width: 400px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto;
|
||||
}
|
||||
|
||||
.error,
|
||||
.input {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
grid-column: 2 / 3;
|
||||
}
|
||||
|
||||
.links {
|
||||
grid-column: 1 / 3;
|
||||
justify-self: center;
|
||||
|
||||
li:not(:last-child) {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
}
|
||||
77
client/public/components/login_form.tsx
Normal file
77
client/public/components/login_form.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { h, type FunctionComponent, type TargetedSubmitEvent } from 'preact'
|
||||
import type { FetchError } from 'rek'
|
||||
import { useCallback } from 'preact/hooks'
|
||||
import { useAuth } from '../../shared/contexts/auth.tsx'
|
||||
import rek from 'rek'
|
||||
|
||||
import useRequestState from '../../shared/hooks/use_request_state.ts'
|
||||
import Input from './input.tsx'
|
||||
import s from './login_form.module.scss'
|
||||
|
||||
const LoginForm: FunctionComponent = () => {
|
||||
const [{ error, pending }, actions] = useRequestState<FetchError>()
|
||||
const { user } = useAuth()
|
||||
|
||||
const login = useCallback(async (e: TargetedSubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
|
||||
actions.pending()
|
||||
|
||||
const form = e.currentTarget
|
||||
|
||||
try {
|
||||
const response = await rek.post(
|
||||
'/auth/login',
|
||||
{
|
||||
email: form.email.value,
|
||||
password: form.password.value,
|
||||
},
|
||||
{ response: false },
|
||||
)
|
||||
|
||||
const lastVisit = response.headers.get('x-last-visit')
|
||||
|
||||
if (lastVisit) {
|
||||
localStorage.setItem('lastVisit', lastVisit)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
user.value = result
|
||||
} catch (err) {
|
||||
actions.error(err as FetchError)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<form onSubmit={login} className={s.root}>
|
||||
{error && <div className={s.error}>Error: {error.body?.message || error.message}</div>}
|
||||
|
||||
<Input className={s.input} name='email' label='Email' placeholder='Email' type='email' autoFocus required />
|
||||
<Input
|
||||
className={s.input}
|
||||
type='password'
|
||||
name='password'
|
||||
label='Password'
|
||||
placeholder='Password'
|
||||
autoComplete='current-password'
|
||||
required
|
||||
/>
|
||||
|
||||
<button className={s.button} type='submit' disabled={pending}>
|
||||
Login
|
||||
</button>
|
||||
|
||||
<ul className={s.links}>
|
||||
<li>
|
||||
<a href='/admin/forgot-password'>Forgot your password?</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='/admin/register'>No account? Register a new</a>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginForm
|
||||
10
client/public/components/login_page.tsx
Normal file
10
client/public/components/login_page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { h } from 'preact'
|
||||
import LoginForm from './login_form.tsx'
|
||||
|
||||
const LoginPage = () => (
|
||||
<section>
|
||||
<LoginForm />
|
||||
</section>
|
||||
)
|
||||
|
||||
export default LoginPage
|
||||
34
client/public/components/route.tsx
Normal file
34
client/public/components/route.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { h, type FunctionComponent } from 'preact'
|
||||
import { useLocation, type RouteProps } from 'preact-iso'
|
||||
import { useAuth } from '../../shared/contexts/auth.tsx'
|
||||
|
||||
type AuthRouteProps = {
|
||||
auth?: boolean
|
||||
user?: any
|
||||
} & RouteProps<any>
|
||||
|
||||
export const Refresh: FunctionComponent<{ path: string }> = () => {
|
||||
location.replace(location.href)
|
||||
}
|
||||
|
||||
export const Redirect: FunctionComponent<{ to: string; replace: boolean }> = ({ to, replace = false }) => {
|
||||
const { route } = useLocation()
|
||||
|
||||
route(to, replace)
|
||||
}
|
||||
|
||||
export const AuthRoute: FunctionComponent<AuthRouteProps> = ({ auth, component, ...props }) => {
|
||||
const { user } = useAuth()
|
||||
|
||||
if (auth === true && !user.value) {
|
||||
return <Redirect to='/login' replace={true} />
|
||||
} else if (auth === false && user.value) {
|
||||
const url = localStorage.getItem('lastVisit') || '/'
|
||||
|
||||
localStorage.removeItem('lastVisit')
|
||||
|
||||
return <Redirect to={url} replace={true} />
|
||||
} else {
|
||||
return h(component, props)
|
||||
}
|
||||
}
|
||||
@ -71,7 +71,9 @@ const TransactionsPage: FunctionComponent = () => {
|
||||
{transactions.map((transaction) => (
|
||||
<tr>
|
||||
<td>{(transaction.transactionDate as unknown as string)?.slice(0, 10)}</td>
|
||||
<td>{transaction.accountNumber}</td>
|
||||
<td>
|
||||
<a href={`/accounts/${transaction.accountNumber}`}>{transaction.accountNumber}</a>
|
||||
</td>
|
||||
<td className='tar'>
|
||||
{(transaction.amount as unknown as number) >= 0
|
||||
? formatNumber(transaction.amount as unknown as number)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import Account from './components/account_page.tsx'
|
||||
import Accounts from './components/accounts_page.tsx'
|
||||
import Balances from './components/balances_page.tsx'
|
||||
import Entries from './components/entries_page.tsx'
|
||||
@ -5,6 +6,7 @@ import Entry from './components/entry_page.tsx'
|
||||
import Invoice from './components/invoice_page.tsx'
|
||||
import Invoices from './components/invoices_page.tsx'
|
||||
import InvoicesBySupplier from './components/invoices_by_supplier_page.tsx'
|
||||
import Login from './components/login_page.tsx'
|
||||
import Objects from './components/objects_page.tsx'
|
||||
import Results from './components/results_page.tsx'
|
||||
import Start from './components/start_page.tsx'
|
||||
@ -16,18 +18,29 @@ export default [
|
||||
name: 'start',
|
||||
title: 'Start',
|
||||
component: Start,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: '/accounts',
|
||||
name: 'accounts',
|
||||
title: 'Konton',
|
||||
component: Accounts,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: '/accounts/:number',
|
||||
name: 'account',
|
||||
title: 'Konto',
|
||||
component: Account,
|
||||
nav: false,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: '/entries',
|
||||
name: 'entries',
|
||||
title: 'Verifikat',
|
||||
component: Entries,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: '/entries/:id',
|
||||
@ -35,18 +48,21 @@ export default [
|
||||
title: 'Verifikat :id',
|
||||
component: Entry,
|
||||
nav: false,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: '/objects',
|
||||
name: 'objects',
|
||||
title: 'Objekt',
|
||||
component: Objects,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: 'invoices',
|
||||
name: 'invoices',
|
||||
title: 'Fakturor',
|
||||
component: Invoices,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: '/invoices/:id',
|
||||
@ -54,6 +70,7 @@ export default [
|
||||
title: 'Faktura :id',
|
||||
component: Invoice,
|
||||
nav: false,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: '/invoices/by-supplier/:supplier',
|
||||
@ -61,23 +78,35 @@ export default [
|
||||
title: 'Fakturor från leverantör',
|
||||
component: InvoicesBySupplier,
|
||||
nav: false,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: '/balances',
|
||||
name: 'balances',
|
||||
title: 'Balanser',
|
||||
component: Balances,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: '/results',
|
||||
name: 'results',
|
||||
title: 'Resultat',
|
||||
component: Results,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: '/transactions',
|
||||
name: 'transactions',
|
||||
title: 'Transaktioner',
|
||||
component: Transactions,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
title: 'Logga in',
|
||||
component: Login,
|
||||
nav: false,
|
||||
auth: false,
|
||||
},
|
||||
]
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
:root {
|
||||
--color-green: green;
|
||||
--color-red: red;
|
||||
--gutter: 16px;
|
||||
--form-valid-color: var(--color-green);
|
||||
--form-invalid-color: var(--color-red);
|
||||
--form-border-color: #d2d6de;
|
||||
--form-element-padding: 9px;
|
||||
--form-label-margin: 6px;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
|
||||
15
client/shared/contexts/auth.tsx
Normal file
15
client/shared/contexts/auth.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { h, createContext, type FunctionComponent } from 'preact'
|
||||
import { useSignal } from '@preact/signals'
|
||||
import { useContext } from 'preact/hooks'
|
||||
|
||||
type AuthContextType = { user: ANY }
|
||||
|
||||
const AuthContext = createContext<AuthContextType | null>(null)
|
||||
|
||||
export const AuthProvider: FunctionComponent<{ user: ANY }> = ({ children, user }) => {
|
||||
const userSignal = useSignal(user)
|
||||
|
||||
return <AuthContext.Provider value={{ user: userSignal }}>{children}</AuthContext.Provider>
|
||||
}
|
||||
|
||||
export const useAuth = () => useContext(AuthContext) as AuthContextType
|
||||
@ -32,8 +32,10 @@
|
||||
"@fastify/session": "^11.1.1",
|
||||
"@fastify/static": "^9.0.0",
|
||||
"@fastify/type-provider-typebox": "^6.1.0",
|
||||
"@preact/signals": "^2.6.2",
|
||||
"chalk": "^5.6.2",
|
||||
"classnames": "^2.5.1",
|
||||
"d3": "^7.9.0",
|
||||
"easy-tz": "^0.2.0",
|
||||
"fastify": "^5.7.2",
|
||||
"fastify-plugin": "^5.1.0",
|
||||
|
||||
330
pnpm-lock.yaml
generated
330
pnpm-lock.yaml
generated
@ -32,12 +32,18 @@ importers:
|
||||
'@fastify/type-provider-typebox':
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(typebox@1.0.80)
|
||||
'@preact/signals':
|
||||
specifier: ^2.6.2
|
||||
version: 2.6.2(preact@10.28.2)
|
||||
chalk:
|
||||
specifier: ^5.6.2
|
||||
version: 5.6.2
|
||||
classnames:
|
||||
specifier: ^2.5.1
|
||||
version: 2.5.1
|
||||
d3:
|
||||
specifier: ^7.9.0
|
||||
version: 7.9.0
|
||||
easy-tz:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
@ -554,21 +560,25 @@ packages:
|
||||
resolution: {integrity: sha512-j4QzfCM8ks+OyM+KKYWDiBEQsm5RCW50H1Wz16wUyoFsobJ+X5qqcJxq6HvkE07m8euYmZelyB0WqsiDoz1v8g==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/linux-arm64-musl@1.42.0':
|
||||
resolution: {integrity: sha512-g5b1Uw7zo6yw4Ymzyd1etKzAY7xAaGA3scwB8tAp3QzuY7CYdfTwlhiLKSAKbd7T/JBgxOXAGNcLDorJyVTXcg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/linux-x64-gnu@1.42.0':
|
||||
resolution: {integrity: sha512-HnD99GD9qAbpV4q9iQil7mXZUJFpoBdDavfcC2CgGLPlawfcV5COzQPNwOgvPVkr7C0cBx6uNCq3S6r9IIiEIg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/linux-x64-musl@1.42.0':
|
||||
resolution: {integrity: sha512-8NTe8A78HHFn+nBi+8qMwIjgv9oIBh+9zqCPNLH56ah4vKOPvbePLI6NIv9qSkmzrBuu8SB+FJ2TH/G05UzbNA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/win32-arm64@1.42.0':
|
||||
resolution: {integrity: sha512-lAPS2YAuu+qFqoTNPFcNsxXjwSV0M+dOgAzzVTAN7Yo2ifj+oLOx0GsntWoM78PvQWI7Q827ZxqtU2ImBmDapA==}
|
||||
@ -609,36 +619,42 @@ packages:
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.6':
|
||||
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.6':
|
||||
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.6':
|
||||
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.6':
|
||||
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.6':
|
||||
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.6':
|
||||
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
|
||||
@ -671,6 +687,14 @@ packages:
|
||||
'@babel/core': 7.x
|
||||
vite: 2.x || 3.x || 4.x || 5.x || 6.x || 7.x
|
||||
|
||||
'@preact/signals-core@1.12.2':
|
||||
resolution: {integrity: sha512-5Yf8h1Ke3SMHr15xl630KtwPTW4sYDFkkxS0vQ8UiQLWwZQnrF9IKaVG1mN5VcJz52EcWs2acsc/Npjha/7ysA==}
|
||||
|
||||
'@preact/signals@2.6.2':
|
||||
resolution: {integrity: sha512-80PMfNS3d8t/J3cRNEJP14zRioWnavgqzX/+tsNGiCX6rpD26+eLkUQpa1sIeOERZMink+dAi34y0MJhg11LKQ==}
|
||||
peerDependencies:
|
||||
preact: '>= 10.25.0 || >=11.0.0-0'
|
||||
|
||||
'@prefresh/babel-plugin@0.5.2':
|
||||
resolution: {integrity: sha512-AOl4HG6dAxWkJ5ndPHBgBa49oo/9bOiJuRDKHLSTyH+Fd9x00shTXpdiTj1W41l6oQIwUOAgJeHMn4QwIDpHkA==}
|
||||
|
||||
@ -735,66 +759,79 @@ packages:
|
||||
resolution: {integrity: sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.57.0':
|
||||
resolution: {integrity: sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.57.0':
|
||||
resolution: {integrity: sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.57.0':
|
||||
resolution: {integrity: sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.57.0':
|
||||
resolution: {integrity: sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.57.0':
|
||||
resolution: {integrity: sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.57.0':
|
||||
resolution: {integrity: sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.57.0':
|
||||
resolution: {integrity: sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.57.0':
|
||||
resolution: {integrity: sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.57.0':
|
||||
resolution: {integrity: sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.57.0':
|
||||
resolution: {integrity: sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.57.0':
|
||||
resolution: {integrity: sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.57.0':
|
||||
resolution: {integrity: sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.57.0':
|
||||
resolution: {integrity: sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==}
|
||||
@ -1078,11 +1115,133 @@ packages:
|
||||
resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
d3-array@3.2.4:
|
||||
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-axis@3.0.0:
|
||||
resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-brush@3.0.0:
|
||||
resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-chord@3.0.1:
|
||||
resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-color@3.1.0:
|
||||
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-contour@4.0.2:
|
||||
resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-delaunay@6.0.4:
|
||||
resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-dispatch@3.0.1:
|
||||
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-drag@3.0.0:
|
||||
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-dsv@3.0.1:
|
||||
resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
d3-ease@3.0.1:
|
||||
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-fetch@3.0.1:
|
||||
resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-force@3.0.0:
|
||||
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-format@3.1.2:
|
||||
resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-geo@3.1.1:
|
||||
resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-hierarchy@3.1.2:
|
||||
resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-interpolate@3.0.1:
|
||||
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-path@3.1.0:
|
||||
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-polygon@3.0.1:
|
||||
resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-quadtree@3.0.1:
|
||||
resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-random@3.0.1:
|
||||
resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-scale-chromatic@3.1.0:
|
||||
resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-scale@4.0.2:
|
||||
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-selection@3.0.0:
|
||||
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-shape@3.2.0:
|
||||
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-time-format@4.1.0:
|
||||
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-time@3.1.0:
|
||||
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-timer@3.0.1:
|
||||
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-transition@3.0.1:
|
||||
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
d3-selection: 2 - 3
|
||||
|
||||
d3-zoom@3.0.0:
|
||||
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3@7.9.0:
|
||||
resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
data-urls@6.0.1:
|
||||
resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==}
|
||||
engines: {node: '>=20'}
|
||||
@ -1111,6 +1270,9 @@ packages:
|
||||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
delaunator@5.0.1:
|
||||
resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
|
||||
|
||||
denque@2.1.0:
|
||||
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||
engines: {node: '>=0.10'}
|
||||
@ -1365,6 +1527,10 @@ packages:
|
||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
internmap@2.0.3:
|
||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ioredis@5.9.2:
|
||||
resolution: {integrity: sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
@ -1808,6 +1974,9 @@ packages:
|
||||
rfdc@1.4.1:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
|
||||
robust-predicates@3.0.2:
|
||||
resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
|
||||
|
||||
rollup@4.57.0:
|
||||
resolution: {integrity: sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
@ -2595,6 +2764,13 @@ snapshots:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
'@preact/signals-core@1.12.2': {}
|
||||
|
||||
'@preact/signals@2.6.2(preact@10.28.2)':
|
||||
dependencies:
|
||||
'@preact/signals-core': 1.12.2
|
||||
preact: 10.28.2
|
||||
|
||||
'@prefresh/babel-plugin@0.5.2': {}
|
||||
|
||||
'@prefresh/core@1.5.9(preact@10.28.2)':
|
||||
@ -2938,12 +3114,158 @@ snapshots:
|
||||
css-tree: 3.1.0
|
||||
lru-cache: 11.2.5
|
||||
|
||||
d3-array@3.2.4:
|
||||
dependencies:
|
||||
internmap: 2.0.3
|
||||
|
||||
d3-axis@3.0.0: {}
|
||||
|
||||
d3-brush@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-drag: 3.0.0
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
|
||||
d3-chord@3.0.1:
|
||||
dependencies:
|
||||
d3-path: 3.1.0
|
||||
|
||||
d3-color@3.1.0: {}
|
||||
|
||||
d3-contour@4.0.2:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
|
||||
d3-delaunay@6.0.4:
|
||||
dependencies:
|
||||
delaunator: 5.0.1
|
||||
|
||||
d3-dispatch@3.0.1: {}
|
||||
|
||||
d3-drag@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
|
||||
d3-dsv@3.0.1:
|
||||
dependencies:
|
||||
commander: 7.2.0
|
||||
iconv-lite: 0.6.3
|
||||
rw: 1.3.3
|
||||
|
||||
d3-ease@3.0.1: {}
|
||||
|
||||
d3-fetch@3.0.1:
|
||||
dependencies:
|
||||
d3-dsv: 3.0.1
|
||||
|
||||
d3-force@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-quadtree: 3.0.1
|
||||
d3-timer: 3.0.1
|
||||
|
||||
d3-format@3.1.2: {}
|
||||
|
||||
d3-geo@3.1.1:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
|
||||
d3-hierarchy@3.1.2: {}
|
||||
|
||||
d3-interpolate@3.0.1:
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
|
||||
d3-path@3.1.0: {}
|
||||
|
||||
d3-polygon@3.0.1: {}
|
||||
|
||||
d3-quadtree@3.0.1: {}
|
||||
|
||||
d3-random@3.0.1: {}
|
||||
|
||||
d3-scale-chromatic@3.1.0:
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
d3-interpolate: 3.0.1
|
||||
|
||||
d3-scale@4.0.2:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
d3-format: 3.1.2
|
||||
d3-interpolate: 3.0.1
|
||||
d3-time: 3.1.0
|
||||
d3-time-format: 4.1.0
|
||||
|
||||
d3-selection@3.0.0: {}
|
||||
|
||||
d3-shape@3.2.0:
|
||||
dependencies:
|
||||
d3-path: 3.1.0
|
||||
|
||||
d3-time-format@4.1.0:
|
||||
dependencies:
|
||||
d3-time: 3.1.0
|
||||
|
||||
d3-time@3.1.0:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
|
||||
d3-timer@3.0.1: {}
|
||||
|
||||
d3-transition@3.0.1(d3-selection@3.0.0):
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
d3-dispatch: 3.0.1
|
||||
d3-ease: 3.0.1
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
d3-zoom@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-drag: 3.0.0
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
|
||||
d3@7.9.0:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
d3-axis: 3.0.0
|
||||
d3-brush: 3.0.0
|
||||
d3-chord: 3.0.1
|
||||
d3-color: 3.1.0
|
||||
d3-contour: 4.0.2
|
||||
d3-delaunay: 6.0.4
|
||||
d3-dispatch: 3.0.1
|
||||
d3-drag: 3.0.0
|
||||
d3-dsv: 3.0.1
|
||||
d3-ease: 3.0.1
|
||||
d3-fetch: 3.0.1
|
||||
d3-force: 3.0.0
|
||||
d3-format: 3.1.2
|
||||
d3-geo: 3.1.1
|
||||
d3-hierarchy: 3.1.2
|
||||
d3-interpolate: 3.0.1
|
||||
d3-path: 3.1.0
|
||||
d3-polygon: 3.0.1
|
||||
d3-quadtree: 3.0.1
|
||||
d3-random: 3.0.1
|
||||
d3-scale: 4.0.2
|
||||
d3-scale-chromatic: 3.1.0
|
||||
d3-selection: 3.0.0
|
||||
d3-shape: 3.2.0
|
||||
d3-time: 3.1.0
|
||||
d3-time-format: 4.1.0
|
||||
d3-timer: 3.0.1
|
||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
d3-zoom: 3.0.0
|
||||
|
||||
data-urls@6.0.1:
|
||||
dependencies:
|
||||
whatwg-mimetype: 5.0.0
|
||||
@ -2988,6 +3310,10 @@ snapshots:
|
||||
has-property-descriptors: 1.0.2
|
||||
object-keys: 1.1.1
|
||||
|
||||
delaunator@5.0.1:
|
||||
dependencies:
|
||||
robust-predicates: 3.0.2
|
||||
|
||||
denque@2.1.0: {}
|
||||
|
||||
depd@2.0.0: {}
|
||||
@ -3270,6 +3596,8 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
side-channel: 1.1.0
|
||||
|
||||
internmap@2.0.3: {}
|
||||
|
||||
ioredis@5.9.2:
|
||||
dependencies:
|
||||
'@ioredis/commands': 1.5.0
|
||||
@ -3711,6 +4039,8 @@ snapshots:
|
||||
|
||||
rfdc@1.4.1: {}
|
||||
|
||||
robust-predicates@3.0.2: {}
|
||||
|
||||
rollup@4.57.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
|
||||
@ -26,9 +26,9 @@ const maxLoginAttempts = 5
|
||||
const requireVerification = true
|
||||
|
||||
const redirects = {
|
||||
login: '/admin/',
|
||||
logout: '/admin/login',
|
||||
register: '/admin/login',
|
||||
login: '/',
|
||||
logout: '/login',
|
||||
register: '/login',
|
||||
}
|
||||
|
||||
const remember = {
|
||||
|
||||
@ -74,6 +74,13 @@ const login: RouteHandler<{ Body: z.infer<typeof BodySchema> }> = async function
|
||||
.where('id', '=', user.id)
|
||||
.execute()
|
||||
|
||||
const lastVisit = request.session.get('lastVisit')
|
||||
|
||||
if (lastVisit) {
|
||||
reply.header('x-last-visit', lastVisit)
|
||||
request.session.set('lastVisit', undefined)
|
||||
}
|
||||
|
||||
return reply.status(200).send(_.omit(user, 'password'))
|
||||
} catch (err) {
|
||||
this.log.error(err)
|
||||
|
||||
@ -9,7 +9,7 @@ const schema: FastifySchema = {
|
||||
const logout: RouteHandler = async function (request, reply) {
|
||||
await request.logout()
|
||||
|
||||
return reply.redirect('/admin/login')
|
||||
return reply.redirect('/login')
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@ -3,6 +3,7 @@ import * as z from 'zod'
|
||||
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
|
||||
|
||||
import { AccountSchema } from '../../schemas/db.ts'
|
||||
import { sql } from 'kysely'
|
||||
|
||||
const accountRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
||||
const { db } = fastify
|
||||
@ -20,6 +21,88 @@ const accountRoutes: FastifyPluginCallbackZod = (fastify, _, done) => {
|
||||
},
|
||||
})
|
||||
|
||||
fastify.route({
|
||||
url: '/:number',
|
||||
method: 'GET',
|
||||
schema: {
|
||||
params: z.object({
|
||||
number: z.coerce.number(),
|
||||
}),
|
||||
// response: {
|
||||
// 200: z.array(AccountSchema),
|
||||
// },
|
||||
},
|
||||
handler(request) {
|
||||
return db
|
||||
.selectFrom('account as a')
|
||||
.innerJoin('transaction as t', 't.accountNumber', 'a.number')
|
||||
.innerJoin('entry as e', 'e.id', 't.entryId')
|
||||
.selectAll('a')
|
||||
.select('e.transactionDate')
|
||||
.where('a.number', '=', request.params.number)
|
||||
.orderBy('e.transactionDate', 'asc')
|
||||
.execute()
|
||||
},
|
||||
})
|
||||
|
||||
fastify.route({
|
||||
url: '/month-sum/:number',
|
||||
method: 'GET',
|
||||
schema: {
|
||||
params: z.object({
|
||||
number: z.coerce.number(),
|
||||
}),
|
||||
|
||||
response: {
|
||||
200: z.array(
|
||||
z.object({
|
||||
month: z.string(),
|
||||
totalAmount: z.coerce.number(),
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
handler(request) {
|
||||
return db
|
||||
.selectFrom('transaction as t')
|
||||
.innerJoin('entry as e', 'e.id', 't.entryId')
|
||||
.select([
|
||||
sql<string>`to_char(date_trunc('month', e."transactionDate"), 'YYYY-MM')`.as('month'),
|
||||
sql<number>`sum(amount)`.as('totalAmount'),
|
||||
])
|
||||
.groupBy(sql`date_trunc('month', e."transactionDate")`)
|
||||
.orderBy('month')
|
||||
.where('t.accountNumber', '=', request.params.number)
|
||||
.execute()
|
||||
},
|
||||
})
|
||||
|
||||
fastify.route({
|
||||
url: '/year-sum/:number',
|
||||
method: 'GET',
|
||||
schema: {
|
||||
params: z.object({
|
||||
number: z.coerce.number(),
|
||||
}),
|
||||
},
|
||||
handler(request) {
|
||||
return (
|
||||
db
|
||||
.selectFrom('transaction as t')
|
||||
.innerJoin('entry as e', 'e.id', 't.entryId')
|
||||
// .select([sql`date_trunc('year', e."transactionDate")`.as('year'), sql<number>`sum(amount)`.as('totalAmount')])
|
||||
.select([
|
||||
sql`to_char(date_trunc('year', e."transactionDate"), 'YYYY')`.as('year'),
|
||||
sql<number>`sum(amount)`.as('totalAmount'),
|
||||
])
|
||||
.groupBy(sql`date_trunc('year', e."transactionDate")`)
|
||||
.orderBy('year')
|
||||
.where('t.accountNumber', '=', request.params.number)
|
||||
.execute()
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
done()
|
||||
}
|
||||
|
||||
|
||||
@ -48,6 +48,19 @@ export default async (options?: FastifyServerOptions) => {
|
||||
server.register(vitePlugin, {
|
||||
mode: env.NODE_ENV,
|
||||
createErrorHandler: ErrorHandler,
|
||||
createPreHandler(route) {
|
||||
return async function preHandler(request, reply) {
|
||||
if (request.session.userId) {
|
||||
return (reply.ctx = { user: await request.user })
|
||||
} else if (route.auth) {
|
||||
if (request.url !== '/') {
|
||||
request.session.set('lastVisit', request.url)
|
||||
}
|
||||
|
||||
return reply.redirect('/login')
|
||||
}
|
||||
}
|
||||
},
|
||||
entries: {
|
||||
public: {
|
||||
path: '/',
|
||||
@ -56,15 +69,6 @@ export default async (options?: FastifyServerOptions) => {
|
||||
admin: {
|
||||
path: '/admin',
|
||||
template: templateAdmin,
|
||||
createPreHandler(route) {
|
||||
return async function preHandler(request, reply) {
|
||||
if (request.session.userId) {
|
||||
return (reply.ctx = { user: await request.user })
|
||||
} else if (route.auth) {
|
||||
return reply.redirect('/admin/login')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -16,7 +16,7 @@ interface Options {
|
||||
}
|
||||
|
||||
export default ({ content, css, head, preload, script, state }: Options) => html`<!DOCTYPE html>
|
||||
<html lang='sv-SE'>
|
||||
<html lang="sv-SE">
|
||||
<head>
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<script type="module" src="${script}"></script>
|
||||
@ -38,6 +38,9 @@ export default ({ content, css, head, preload, script, state }: Options) => html
|
||||
<body>
|
||||
${content}
|
||||
${state?.then((state) => `<script>window.__STATE__ = ${JSON.stringify(state)}</script>`)}
|
||||
<script>var offset=window.history.state&&window.history.state.scrollTop||0;if(offset)window.scrollTo(0,offset)</script>
|
||||
<script>
|
||||
var offset = (window.history.state && window.history.state.scrollTop) || 0;
|
||||
if (offset) window.scrollTo(0, offset);
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
</html>`
|
||||
|
||||
1
shared/global.d.ts
vendored
1
shared/global.d.ts
vendored
@ -13,6 +13,7 @@ declare global {
|
||||
|
||||
declare module 'fastify' {
|
||||
interface Session {
|
||||
lastVisit?: string
|
||||
userId: number
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user