Got working preact SSR
parent
5142d4c551
commit
ddad7fe746
@ -1 +1,2 @@
|
|||||||
auto-install-peers=true
|
# Needed by pnpm to work with "@preact/preset-vite"
|
||||||
|
shamefully-hoist=true
|
||||||
|
@ -1,13 +1,30 @@
|
|||||||
import Router from 'preact-router'
|
import Router from 'preact-router'
|
||||||
|
import { route } from 'preact-router'
|
||||||
|
import { useEffect } from 'preact/hooks'
|
||||||
|
|
||||||
import { HomePage } from './pages/Home.jsx'
|
import { HomePage } from './pages/Home.jsx'
|
||||||
import { LoginPage } from './pages/Login.jsx'
|
import { LoginPage } from './pages/Login.jsx'
|
||||||
import { ProblemPage } from './pages/Problem.jsx'
|
import { ProblemPage } from './pages/Problem.jsx'
|
||||||
|
|
||||||
export const App = ({ path }) => (
|
const Redirect = ({ to }) => {
|
||||||
<Router>
|
useEffect(() => {
|
||||||
|
route(to, true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
Redirecting to <pre>{to}</pre>...
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const App = ({ url }) => {
|
||||||
|
return (
|
||||||
|
<Router url={url}>
|
||||||
<HomePage path="/" />
|
<HomePage path="/" />
|
||||||
<LoginPage path="/login" />
|
<LoginPage path="/login" />
|
||||||
<ProblemPage path="/problem/:id" />
|
<ProblemPage path="/problem/:id" />
|
||||||
|
<Redirect default to="/" />
|
||||||
</Router>
|
</Router>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
import { useSignal } from '@preact/signals'
|
|
||||||
import { useEffect, useMemo } from 'preact/hooks'
|
|
||||||
|
|
||||||
import URLPattern from 'url-pattern'
|
|
||||||
|
|
||||||
export const Router = ({ pages }) => {
|
|
||||||
const compiledRoutes = useMemo(
|
|
||||||
() =>
|
|
||||||
Object.entries(pages).map(([pattern, Page]) => ({
|
|
||||||
pattern: new URLPattern(pattern),
|
|
||||||
Page,
|
|
||||||
})),
|
|
||||||
[pages]
|
|
||||||
)
|
|
||||||
|
|
||||||
const routerUrl = useSignal(location.hash.slice(1))
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener('hashchange', () => {
|
|
||||||
routerUrl.value = location.hash.slice(1)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const route = compiledRoutes.flatMap(({ pattern, Page }) => {
|
|
||||||
const m = pattern.match(routerUrl.value.split('?', 1)[0])
|
|
||||||
return m ? [{ Page, params: m }] : []
|
|
||||||
})?.[0]
|
|
||||||
|
|
||||||
if (!route) {
|
|
||||||
console.log(`Invalid route "${routerUrl.value}", redirecting to homepage`)
|
|
||||||
|
|
||||||
location.href = '/#/'
|
|
||||||
routerUrl.value = '/'
|
|
||||||
|
|
||||||
return <>Redirecting...</>
|
|
||||||
}
|
|
||||||
|
|
||||||
const { Page, params } = route
|
|
||||||
|
|
||||||
const queryPart =
|
|
||||||
routerUrl.value.indexOf('?') === -1
|
|
||||||
? ''
|
|
||||||
: routerUrl.value.slice(routerUrl.value.indexOf('?') + 1)
|
|
||||||
console.log(queryPart)
|
|
||||||
|
|
||||||
const queryParams =
|
|
||||||
queryPart?.length > 0
|
|
||||||
? Object.fromEntries(
|
|
||||||
queryPart.split('&').map(kvPart => {
|
|
||||||
const eqIndex = kvPart.indexOf('=')
|
|
||||||
return eqIndex === -1
|
|
||||||
? [kvPart, true]
|
|
||||||
: [
|
|
||||||
kvPart.slice(0, eqIndex),
|
|
||||||
decodeURIComponent(kvPart.slice(eqIndex + 1)),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
)
|
|
||||||
: {}
|
|
||||||
|
|
||||||
return <Page params={params} query={queryParams} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Link = ({ page, params, query, children }) => {
|
|
||||||
for (const [key, value] of Object.entries(params ?? {})) {
|
|
||||||
page = page.replace(':' + key, encodeURIComponent(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetHref = page
|
|
||||||
if (query) {
|
|
||||||
targetHref +=
|
|
||||||
'?' +
|
|
||||||
Object.entries(query ?? {})
|
|
||||||
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
|
||||||
.join('&')
|
|
||||||
}
|
|
||||||
|
|
||||||
return <a href={'/#' + targetHref}>{children}</a>
|
|
||||||
}
|
|
@ -0,0 +1,4 @@
|
|||||||
|
import { hydrate } from 'preact'
|
||||||
|
import { App } from './App.jsx'
|
||||||
|
|
||||||
|
hydrate(<App />, document.body)
|
@ -0,0 +1,6 @@
|
|||||||
|
import renderToString from 'preact-render-to-string'
|
||||||
|
import { App } from './App.jsx'
|
||||||
|
|
||||||
|
export function render(url) {
|
||||||
|
return renderToString(<App url={url} />)
|
||||||
|
}
|
@ -1,15 +1,24 @@
|
|||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
|
|
||||||
export const useUser = () => {
|
export const useUser = () => {
|
||||||
const [username, setUsername] = useState(null)
|
const [user, setUser] = useState(null)
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
setUser(null)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(async () => {
|
useEffect(async () => {
|
||||||
const res = await fetch(`/api/current-user`, {
|
const res = await fetch(`/api/current-user`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
const username = await res.json()
|
|
||||||
setUsername(username)
|
if (res.ok) {
|
||||||
|
const user = await res.json()
|
||||||
|
setUser(user)
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return { username }
|
console.log(user)
|
||||||
|
|
||||||
|
return [user, logout]
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
export const initialDatabaseValue = {
|
||||||
|
users: {
|
||||||
|
['BachoSeven']: {},
|
||||||
|
['aziis98']: {},
|
||||||
|
},
|
||||||
|
problems: {},
|
||||||
|
}
|
@ -1,79 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
|
|
||||||
import crypto from 'crypto'
|
|
||||||
|
|
||||||
// import serveStatic from 'serve-static'
|
|
||||||
import bodyParser from 'body-parser'
|
|
||||||
import cookieParser from 'cookie-parser'
|
|
||||||
|
|
||||||
import { authMiddleware, loggingMiddleware, PingRouter, StatusRouter } from './server/routes.js'
|
|
||||||
import { createDatabase, getUser, updateUser } from './server/db/database.js'
|
|
||||||
|
|
||||||
const app = express()
|
|
||||||
|
|
||||||
const sessions = {
|
|
||||||
store: {},
|
|
||||||
createSession(username) {
|
|
||||||
const sid = crypto.randomBytes(10).toString('hex')
|
|
||||||
this.store[sid] = username
|
|
||||||
return sid
|
|
||||||
},
|
|
||||||
getUserForSession(sid) {
|
|
||||||
return this.store[sid] ?? null
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = createDatabase('./db.local.json', {
|
|
||||||
users: {
|
|
||||||
['BachoSeven']: {},
|
|
||||||
['aziis98']: {},
|
|
||||||
},
|
|
||||||
problems: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use(bodyParser.json())
|
|
||||||
app.use(cookieParser())
|
|
||||||
|
|
||||||
app.use(loggingMiddleware)
|
|
||||||
app.use(authMiddleware(sid => sessions.getUserForSession(sid)))
|
|
||||||
|
|
||||||
app.use('/api/status', new StatusRouter())
|
|
||||||
app.use('/api/ping', new PingRouter())
|
|
||||||
|
|
||||||
app.get('/api/current-user', (req, res) => {
|
|
||||||
res.json(sessions.getUserForSession(req.cookies.sid))
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post('/api/login', (req, res) => {
|
|
||||||
const { username } = req.body
|
|
||||||
res.cookie('sid', sessions.createSession(username), { maxAge: 1000 * 60 * 60 * 24 * 7 })
|
|
||||||
res.json({ status: 'ok' })
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post('/api/logout', (req, res) => {
|
|
||||||
res.cookie('sid', '', { expires: new Date() })
|
|
||||||
res.json({ status: 'ok' })
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get('/api/user/:id', async (req, res) => {
|
|
||||||
const user = await getUser(db, req.params.id)
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
res.json(user)
|
|
||||||
} else {
|
|
||||||
res.sendStatus(404)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post('/api/user/:id', async (req, res) => {
|
|
||||||
await updateUser(db, req.params.id, req.body)
|
|
||||||
res.sendStatus(200)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.all('*', (_req, res) => {
|
|
||||||
res.sendStatus(404)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.listen(4000, () => {
|
|
||||||
console.log(`Started server on port 4000...`)
|
|
||||||
})
|
|
@ -0,0 +1,86 @@
|
|||||||
|
import crypto from 'crypto'
|
||||||
|
|
||||||
|
import bodyParser from 'body-parser'
|
||||||
|
import cookieParser from 'cookie-parser'
|
||||||
|
|
||||||
|
import { authMiddleware, loggingMiddleware, PingRouter, StatusRouter } from './middlewares.js'
|
||||||
|
import { createDatabase, getUser, updateUser } from './db/database.js'
|
||||||
|
import express from 'express'
|
||||||
|
import { initialDatabaseValue } from './db/example-data.js'
|
||||||
|
import { useId } from 'preact/hooks'
|
||||||
|
|
||||||
|
export async function createApiRouter() {
|
||||||
|
const sessions = {
|
||||||
|
store: {},
|
||||||
|
createSession(username) {
|
||||||
|
const sid = crypto.randomBytes(10).toString('hex')
|
||||||
|
this.store[sid] = username
|
||||||
|
return sid
|
||||||
|
},
|
||||||
|
getUserForSession(sid) {
|
||||||
|
return this.store[sid] ?? null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = createDatabase('./db.local.json', initialDatabaseValue)
|
||||||
|
|
||||||
|
const r = express.Router()
|
||||||
|
|
||||||
|
r.use(bodyParser.json())
|
||||||
|
r.use(cookieParser())
|
||||||
|
|
||||||
|
r.use(loggingMiddleware)
|
||||||
|
r.use(authMiddleware(sid => sessions.getUserForSession(sid)))
|
||||||
|
|
||||||
|
r.use('/api/status', new StatusRouter())
|
||||||
|
r.use('/api/ping', new PingRouter())
|
||||||
|
|
||||||
|
r.get('/api/current-user', async (req, res) => {
|
||||||
|
const userId = sessions.getUserForSession(req.cookies.sid)
|
||||||
|
if (!userId) {
|
||||||
|
res.cookie('sid', '', { expires: new Date() })
|
||||||
|
res.status(400)
|
||||||
|
res.end('Invalid session token')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await getUser(db, userId)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
username: userId,
|
||||||
|
...user,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
r.post('/api/login', (req, res) => {
|
||||||
|
const { username } = req.body
|
||||||
|
res.cookie('sid', sessions.createSession(username), { maxAge: 1000 * 60 * 60 * 24 * 7 })
|
||||||
|
res.json({ status: 'ok' })
|
||||||
|
})
|
||||||
|
|
||||||
|
r.post('/api/logout', (req, res) => {
|
||||||
|
res.cookie('sid', '', { expires: new Date() })
|
||||||
|
res.json({ status: 'ok' })
|
||||||
|
})
|
||||||
|
|
||||||
|
r.get('/api/user/:id', async (req, res) => {
|
||||||
|
const user = await getUser(db, req.params.id)
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
res.json(user)
|
||||||
|
} else {
|
||||||
|
res.sendStatus(404)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
r.post('/api/user/:id', async (req, res) => {
|
||||||
|
await updateUser(db, req.params.id, req.body)
|
||||||
|
res.sendStatus(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
// r.all('*', (_req, res) => {
|
||||||
|
// res.sendStatus(404)
|
||||||
|
// })
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
Loading…
Reference in New Issue