Refactoring to Typescript...
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import Router from 'preact-router'
|
||||
import { route } from 'preact-router'
|
||||
import { useEffect } from 'preact/hooks'
|
||||
import { AdminPage } from './pages/Admin.jsx'
|
||||
import { AdminPage } from './pages/Admin'
|
||||
|
||||
import { HomePage } from './pages/Home.jsx'
|
||||
import { LoginPage } from './pages/Login.jsx'
|
||||
import { ProblemPage } from './pages/Problem.jsx'
|
||||
import { ProfilePage } from './pages/Profile.jsx'
|
||||
import { HomePage } from './pages/Home'
|
||||
import { LoginPage } from './pages/Login'
|
||||
import { ProblemPage } from './pages/Problem'
|
||||
import { ProfilePage } from './pages/Profile'
|
||||
|
||||
const Redirect = ({ to }) => {
|
||||
const Redirect = ({ to }: { default: boolean; to: string }) => {
|
||||
useEffect(() => {
|
||||
route(to, true)
|
||||
}, [])
|
||||
@@ -20,7 +20,7 @@ const Redirect = ({ to }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const App = ({ url }) => {
|
||||
export const App = ({ url }: { url?: string }) => {
|
||||
return (
|
||||
<Router url={url}>
|
||||
<HomePage path="/" />
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Link } from 'preact-router/match'
|
||||
import { isAdministrator, USER_ROLE_ADMIN, USER_ROLE_MODERATOR } from '../../shared/constants.js'
|
||||
|
||||
const ROLE_LABEL = {
|
||||
admin: 'Admin',
|
||||
moderator: 'Moderatore',
|
||||
[USER_ROLE_ADMIN]: 'Admin',
|
||||
[USER_ROLE_MODERATOR]: 'Moderatore',
|
||||
}
|
||||
|
||||
export const Header = ({ user, noLogin }) => (
|
||||
@@ -13,7 +14,7 @@ export const Header = ({ user, noLogin }) => (
|
||||
<nav>
|
||||
{user ? (
|
||||
<>
|
||||
{user.role !== 'student' && (
|
||||
{isAdministrator(user.role) && (
|
||||
<div class="nav-item">
|
||||
<Link activeClassName="active" href="/admin">
|
||||
Pannello Admin
|
||||
@@ -22,8 +23,8 @@ export const Header = ({ user, noLogin }) => (
|
||||
)}
|
||||
<div class="nav-item">
|
||||
<Link activeClassName="active" href="/profile">
|
||||
@{user.username}
|
||||
{user.role !== 'student' && <> ({ROLE_LABEL[user.role]})</>}
|
||||
@{user.id}
|
||||
{isAdministrator(user.role) && <> ({ROLE_LABEL[user.role]})</>}
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Markdown } from './Markdown.jsx'
|
||||
|
||||
export const Problem = ({ id, content, createdBy }) => {
|
||||
export const Problem = ({ id, content, solutionsCount }) => {
|
||||
return (
|
||||
<div class="problem">
|
||||
<div class="problem-header">
|
||||
@@ -11,6 +11,11 @@ export const Problem = ({ id, content, createdBy }) => {
|
||||
<div class="problem-content">
|
||||
<Markdown source={content} />
|
||||
</div>
|
||||
{solutionsCount > 0 && (
|
||||
<div class="problem-footer">
|
||||
{solutionsCount} soluzion{solutionsCount === 1 ? 'e' : 'i'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { refToUserId } from '../../shared/db-refs.js'
|
||||
import { Markdown } from './Markdown.jsx'
|
||||
|
||||
export const Solution = ({ userId, problemId, content }) => {
|
||||
export const Solution = ({ sentBy, forProblem, content }) => {
|
||||
const userId = refToUserId(sentBy)
|
||||
const problemId = refToUserId(forProblem)
|
||||
|
||||
return (
|
||||
<div class="solution">
|
||||
<div class="solution-header">
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { hydrate } from 'preact'
|
||||
import { App } from './App.jsx'
|
||||
|
||||
hydrate(<App />, document.body)
|
||||
@@ -0,0 +1,4 @@
|
||||
import { hydrate } from 'preact'
|
||||
import { App } from './App'
|
||||
|
||||
// hydrate(<App />, document.body)
|
||||
@@ -1,13 +1,15 @@
|
||||
import renderToString from 'preact-render-to-string'
|
||||
import { App } from './App.jsx'
|
||||
import { MetadataContext } from './hooks.jsx'
|
||||
// import { App } from './App'
|
||||
import { MetadataContext } from './hooks'
|
||||
|
||||
export function render(url) {
|
||||
import { RenderedPage } from '../shared/ssr'
|
||||
|
||||
export default (url: string): RenderedPage => {
|
||||
const metadata = {}
|
||||
|
||||
const html = renderToString(
|
||||
<MetadataContext.Provider value={metadata}>
|
||||
<App url={url} />
|
||||
{/* <App url={url} /> */}
|
||||
</MetadataContext.Provider>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { route } from 'preact-router'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { isAdministrator, isStudent } from '../../shared/constants.js'
|
||||
import { server } from '../api.jsx'
|
||||
import { Header } from '../components/Header.jsx'
|
||||
import { MarkdownEditor } from '../components/MarkdownEditor.jsx'
|
||||
@@ -27,7 +28,7 @@ export const AdminPage = ({}) => {
|
||||
const [user] = useCurrentUser(user => {
|
||||
if (!user) {
|
||||
route('/login', true)
|
||||
} else if (user.role !== 'admin' && user.role !== 'moderator') {
|
||||
} else if (isStudent(user.role)) {
|
||||
route('/', true)
|
||||
}
|
||||
})
|
||||
@@ -10,6 +10,8 @@ export const HomePage = () => {
|
||||
const [user] = useCurrentUser()
|
||||
const [problems] = useReadResource('/api/problems', [])
|
||||
|
||||
console.log(problems)
|
||||
|
||||
return (
|
||||
<main class="page-home">
|
||||
<Header {...{ user }} />
|
||||
@@ -13,7 +13,7 @@ export const LoginPage = () => {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
id: username,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ export const ProblemPage = ({ id }) => {
|
||||
|
||||
const sendSolution = async () => {
|
||||
await server.post('/api/solution', {
|
||||
problemId: id,
|
||||
forProblem: id,
|
||||
content: source,
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ export const ProfilePage = ({}) => {
|
||||
route('/login', true)
|
||||
}
|
||||
|
||||
setSolutions(await server.get(`/api/solutions?user=${user.username}`))
|
||||
setSolutions(await server.get(`/api/solutions?user=${user.id}`))
|
||||
})
|
||||
|
||||
const handleLogout = () => {
|
||||
@@ -27,8 +27,8 @@ export const ProfilePage = ({}) => {
|
||||
<Header {...{ user }} />
|
||||
<div class="subtitle">Le tue soluzioni</div>
|
||||
<div class="solution-list">
|
||||
{solutions.map(({ problemId, content }) => (
|
||||
<Solution {...{ problemId, content }} />
|
||||
{solutions.map(({ forProblem, content }) => (
|
||||
<Solution {...{ forProblem, content }} />
|
||||
))}
|
||||
</div>
|
||||
<div class="subtitle">Altro</div>
|
||||
+16
-9
@@ -22,7 +22,7 @@ body {
|
||||
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -70,7 +70,7 @@ input[type='text'] {
|
||||
cursor: pointer;
|
||||
font-family: 'Lato';
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
|
||||
border: 1px solid #c8c8c8;
|
||||
@@ -102,7 +102,7 @@ input[type='text'] {
|
||||
|
||||
font-family: 'Lato';
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ button {
|
||||
|
||||
font-family: 'Lato';
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
|
||||
border: 1px solid #c8c8c8;
|
||||
@@ -199,7 +199,7 @@ $heading-scale: 1.25;
|
||||
.text-body {
|
||||
font-family: 'EB Garamond', serif;
|
||||
font-weight: 400;
|
||||
font-size: 21px;
|
||||
font-size: 18px;
|
||||
|
||||
line-height: 1.41;
|
||||
|
||||
@@ -232,7 +232,7 @@ main {
|
||||
gap: 2rem;
|
||||
|
||||
.subtitle {
|
||||
font-size: 24px;
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +301,9 @@ header {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
|
||||
.nav-item {
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
@@ -327,7 +330,7 @@ header {
|
||||
background: #ffffff;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: 0.5rem;
|
||||
|
||||
.problem-header {
|
||||
@@ -336,7 +339,7 @@ header {
|
||||
gap: 0.25rem;
|
||||
|
||||
.problem-title {
|
||||
font-size: 24px;
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
@@ -344,6 +347,10 @@ header {
|
||||
.problem-content {
|
||||
@extend .text-body;
|
||||
}
|
||||
|
||||
.problem-footer {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.solution {
|
||||
@@ -420,7 +427,7 @@ header {
|
||||
|
||||
textarea {
|
||||
font-family: 'DM Mono', monospace;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
|
||||
resize: none;
|
||||
overflow-y: hidden;
|
||||
|
||||
+1
-1
@@ -30,6 +30,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<!-- SSR OUTLET -->
|
||||
<script type="module" src="/client/entry-client.jsx"></script>
|
||||
<script type="module" src="/client/entry-client.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+14
-6
@@ -5,11 +5,13 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "MODE=development node server.js",
|
||||
"build": "run-s build:client build:server",
|
||||
"build:client": "vite build --outDir dist/client",
|
||||
"build:server": "vite build --ssr client/entry-server.jsx --outDir dist/server",
|
||||
"serve": "node server.js"
|
||||
"dev": "run-s build:server serve:dev",
|
||||
"build": "run-s build:client build:ssr build:server",
|
||||
"build:server": "esbuild server.ts --bundle --platform=node --format=esm --external:./node_modules/* --outdir=dist/server",
|
||||
"build:client": "vite build --outDir dist/entry-client",
|
||||
"build:ssr": "vite build --ssr client/entry-server.tsx --outDir dist/entry-server",
|
||||
"serve:dev": "MODE=development node dist/server/server.js",
|
||||
"serve": "node dist/server/server.js"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -35,7 +37,13 @@
|
||||
"vite": "^3.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@types/node": "^18.11.9",
|
||||
"concurrently": "^7.5.0",
|
||||
"npm-run-all": "^4.1.5"
|
||||
"esbuild": "^0.15.13",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+83
-23
@@ -3,10 +3,15 @@ lockfileVersion: 5.4
|
||||
specifiers:
|
||||
'@preact/preset-vite': ^2.4.0
|
||||
'@preact/signals': ^1.1.2
|
||||
'@types/cookie-parser': ^1.4.3
|
||||
'@types/express': ^4.17.14
|
||||
'@types/morgan': ^1.9.3
|
||||
'@types/node': ^18.11.9
|
||||
body-parser: ^1.20.1
|
||||
chalk: ^5.1.2
|
||||
concurrently: ^7.5.0
|
||||
cookie-parser: ^1.4.6
|
||||
esbuild: ^0.15.13
|
||||
express: ^4.18.2
|
||||
katex: ^0.16.3
|
||||
morgan: ^1.10.0
|
||||
@@ -20,6 +25,7 @@ specifiers:
|
||||
remark-parse: ^10.0.1
|
||||
remark-rehype: ^10.1.0
|
||||
sass: ^1.55.0
|
||||
typescript: ^4.8.4
|
||||
unified: ^10.1.2
|
||||
url-pattern: ^1.0.3
|
||||
vite: ^3.2.2
|
||||
@@ -47,8 +53,14 @@ dependencies:
|
||||
vite: 3.2.2_sass@1.56.0
|
||||
|
||||
devDependencies:
|
||||
'@types/cookie-parser': 1.4.3
|
||||
'@types/express': 4.17.14
|
||||
'@types/morgan': 1.9.3
|
||||
'@types/node': 18.11.9
|
||||
concurrently: 7.5.0
|
||||
esbuild: 0.15.13
|
||||
npm-run-all: 4.1.5
|
||||
typescript: 4.8.4
|
||||
|
||||
packages:
|
||||
|
||||
@@ -302,7 +314,6 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-loong64/0.15.13:
|
||||
@@ -311,7 +322,6 @@ packages:
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@jridgewell/gen-mapping/0.1.1:
|
||||
@@ -426,12 +436,48 @@ packages:
|
||||
picomatch: 2.3.1
|
||||
dev: false
|
||||
|
||||
/@types/body-parser/1.19.2:
|
||||
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
|
||||
dependencies:
|
||||
'@types/connect': 3.4.35
|
||||
'@types/node': 18.11.9
|
||||
dev: true
|
||||
|
||||
/@types/connect/3.4.35:
|
||||
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
|
||||
dependencies:
|
||||
'@types/node': 18.11.9
|
||||
dev: true
|
||||
|
||||
/@types/cookie-parser/1.4.3:
|
||||
resolution: {integrity: sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.14
|
||||
dev: true
|
||||
|
||||
/@types/debug/4.1.7:
|
||||
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
|
||||
dependencies:
|
||||
'@types/ms': 0.7.31
|
||||
dev: false
|
||||
|
||||
/@types/express-serve-static-core/4.17.31:
|
||||
resolution: {integrity: sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==}
|
||||
dependencies:
|
||||
'@types/node': 18.11.9
|
||||
'@types/qs': 6.9.7
|
||||
'@types/range-parser': 1.2.4
|
||||
dev: true
|
||||
|
||||
/@types/express/4.17.14:
|
||||
resolution: {integrity: sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==}
|
||||
dependencies:
|
||||
'@types/body-parser': 1.19.2
|
||||
'@types/express-serve-static-core': 4.17.31
|
||||
'@types/qs': 6.9.7
|
||||
'@types/serve-static': 1.15.0
|
||||
dev: true
|
||||
|
||||
/@types/hast/2.3.4:
|
||||
resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==}
|
||||
dependencies:
|
||||
@@ -448,14 +494,43 @@ packages:
|
||||
'@types/unist': 2.0.6
|
||||
dev: false
|
||||
|
||||
/@types/mime/3.0.1:
|
||||
resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
|
||||
dev: true
|
||||
|
||||
/@types/morgan/1.9.3:
|
||||
resolution: {integrity: sha512-BiLcfVqGBZCyNCnCH3F4o2GmDLrpy0HeBVnNlyZG4fo88ZiE9SoiBe3C+2ezuwbjlEyT+PDZ17//TAlRxAn75Q==}
|
||||
dependencies:
|
||||
'@types/node': 18.11.9
|
||||
dev: true
|
||||
|
||||
/@types/ms/0.7.31:
|
||||
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
|
||||
dev: false
|
||||
|
||||
/@types/node/18.11.9:
|
||||
resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==}
|
||||
dev: true
|
||||
|
||||
/@types/parse5/6.0.3:
|
||||
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
|
||||
dev: false
|
||||
|
||||
/@types/qs/6.9.7:
|
||||
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
|
||||
dev: true
|
||||
|
||||
/@types/range-parser/1.2.4:
|
||||
resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==}
|
||||
dev: true
|
||||
|
||||
/@types/serve-static/1.15.0:
|
||||
resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==}
|
||||
dependencies:
|
||||
'@types/mime': 3.0.1
|
||||
'@types/node': 18.11.9
|
||||
dev: true
|
||||
|
||||
/@types/unist/2.0.6:
|
||||
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
|
||||
dev: false
|
||||
@@ -872,7 +947,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-android-arm64/0.15.13:
|
||||
@@ -881,7 +955,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-darwin-64/0.15.13:
|
||||
@@ -890,7 +963,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-darwin-arm64/0.15.13:
|
||||
@@ -899,7 +971,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-freebsd-64/0.15.13:
|
||||
@@ -908,7 +979,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-freebsd-arm64/0.15.13:
|
||||
@@ -917,7 +987,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-32/0.15.13:
|
||||
@@ -926,7 +995,6 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-64/0.15.13:
|
||||
@@ -935,7 +1003,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-arm/0.15.13:
|
||||
@@ -944,7 +1011,6 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-arm64/0.15.13:
|
||||
@@ -953,7 +1019,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-mips64le/0.15.13:
|
||||
@@ -962,7 +1027,6 @@ packages:
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-ppc64le/0.15.13:
|
||||
@@ -971,7 +1035,6 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-riscv64/0.15.13:
|
||||
@@ -980,7 +1043,6 @@ packages:
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-linux-s390x/0.15.13:
|
||||
@@ -989,7 +1051,6 @@ packages:
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-netbsd-64/0.15.13:
|
||||
@@ -998,7 +1059,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-openbsd-64/0.15.13:
|
||||
@@ -1007,7 +1067,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-sunos-64/0.15.13:
|
||||
@@ -1016,7 +1075,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-windows-32/0.15.13:
|
||||
@@ -1025,7 +1083,6 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-windows-64/0.15.13:
|
||||
@@ -1034,7 +1091,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild-windows-arm64/0.15.13:
|
||||
@@ -1043,7 +1099,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/esbuild/0.15.13:
|
||||
@@ -1074,7 +1129,6 @@ packages:
|
||||
esbuild-windows-32: 0.15.13
|
||||
esbuild-windows-64: 0.15.13
|
||||
esbuild-windows-arm64: 0.15.13
|
||||
dev: false
|
||||
|
||||
/escalade/3.1.1:
|
||||
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
||||
@@ -2489,6 +2543,12 @@ packages:
|
||||
mime-types: 2.1.35
|
||||
dev: false
|
||||
|
||||
/typescript/4.8.4:
|
||||
resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==}
|
||||
engines: {node: '>=4.2.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/unbox-primitive/1.0.2:
|
||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||
dependencies:
|
||||
|
||||
@@ -7,6 +7,7 @@ import morgan from 'morgan'
|
||||
import { createServer as createViteServer } from 'vite'
|
||||
|
||||
import { createApiRouter } from './server/routes.js'
|
||||
import { RenderFunction } from './shared/ssr.js'
|
||||
|
||||
const HTML_ROUTES = ['/', '/login', '/problem/:id', '/admin', '/profile']
|
||||
|
||||
@@ -46,7 +47,8 @@ async function createDevRouter() {
|
||||
const transformedTemplate = await vite.transformIndexHtml(req.originalUrl, indexHtml)
|
||||
|
||||
// Load (to be bundled) entry point for server side rendering
|
||||
const { render } = await vite.ssrLoadModule('./client/entry-server.jsx')
|
||||
const render: RenderFunction = (await vite.ssrLoadModule('./client/entry-server.tsx'))
|
||||
.default
|
||||
|
||||
const { html, metadata } = render(req.originalUrl)
|
||||
|
||||
@@ -80,9 +82,10 @@ async function createDevRouter() {
|
||||
|
||||
async function createProductionRouter() {
|
||||
// Load bundled entry point for server side rendering
|
||||
const { render } = await import('./dist/server/entry-server.js')
|
||||
const render: RenderFunction = ((await import('./dist/entry-server/entry-server.js')) as any)
|
||||
.default
|
||||
|
||||
const r = new express.Router()
|
||||
const r = express.Router()
|
||||
|
||||
r.use('/', express.static('dist/client'))
|
||||
|
||||
@@ -2,21 +2,39 @@ import crypto from 'crypto'
|
||||
|
||||
import { readFile, writeFile, access, constants } from 'fs/promises'
|
||||
|
||||
function once(fn, message) {
|
||||
import {
|
||||
CommonProps as MetaProps,
|
||||
Problem,
|
||||
ProblemId,
|
||||
Solution,
|
||||
SolutionId,
|
||||
User,
|
||||
UserId,
|
||||
} from '../../shared/model'
|
||||
|
||||
function once<T extends (...args: any) => any>(fn: T, message: string): T {
|
||||
let flag = false
|
||||
|
||||
return (...args) => {
|
||||
return ((...args: any) => {
|
||||
if (flag) {
|
||||
throw new Error(message ?? `cannot run more than once`)
|
||||
}
|
||||
flag = true
|
||||
return fn(...args)
|
||||
}
|
||||
}) as T
|
||||
}
|
||||
|
||||
function createMutex() {
|
||||
interface Lock {
|
||||
(): void
|
||||
}
|
||||
|
||||
interface Mutex {
|
||||
lock(): Promise<Lock>
|
||||
}
|
||||
|
||||
function createMutex(): Mutex {
|
||||
let locked = false
|
||||
const waiters = []
|
||||
const waiters: ((fn: () => void) => void)[] = []
|
||||
|
||||
const unlock = () => {
|
||||
if (waiters.length > 0) {
|
||||
@@ -29,7 +47,7 @@ function createMutex() {
|
||||
}
|
||||
}
|
||||
|
||||
const lock = () => {
|
||||
const lock = (): Promise<Lock> => {
|
||||
if (locked) {
|
||||
console.log(`[Mutex] Putting into queue`)
|
||||
return new Promise(resolve => {
|
||||
@@ -38,20 +56,26 @@ function createMutex() {
|
||||
} else {
|
||||
console.log(`[Mutex] Acquiring the lock`)
|
||||
locked = true
|
||||
return once(unlock, `lock already released`)
|
||||
return Promise.resolve(once(unlock, `lock already released`))
|
||||
}
|
||||
}
|
||||
|
||||
return { lock }
|
||||
}
|
||||
|
||||
// l = createLock()
|
||||
// unlock1 = await l.lock() // immediate
|
||||
// unlock2 = await l.lock() // hangs...
|
||||
// unlock1() // l2 restarts...
|
||||
// unlock2() // no waiters so noop
|
||||
type DatabaseConnection = {
|
||||
path: string
|
||||
initialValue: Database
|
||||
mu: Mutex
|
||||
}
|
||||
|
||||
export function createDatabase(path, initialValue) {
|
||||
export type Database = {
|
||||
users: Record<string, User>
|
||||
problems: Record<string, Problem>
|
||||
solutions: Record<string, Solution>
|
||||
}
|
||||
|
||||
export function createDatabase(path: string, initialValue: Database) {
|
||||
return {
|
||||
path,
|
||||
initialValue,
|
||||
@@ -59,8 +83,11 @@ export function createDatabase(path, initialValue) {
|
||||
}
|
||||
}
|
||||
|
||||
async function withDatabase({ path, initialValue, mu }, fn) {
|
||||
const unlock = await mu.lock()
|
||||
async function withDatabase<R>(
|
||||
{ path, initialValue, mu }: DatabaseConnection,
|
||||
fn: (db: Database) => R | Promise<R>
|
||||
): Promise<R> {
|
||||
const unlock: Lock = await mu.lock()
|
||||
|
||||
try {
|
||||
await access(path, constants.R_OK)
|
||||
@@ -82,43 +109,30 @@ async function withDatabase({ path, initialValue, mu }, fn) {
|
||||
return result
|
||||
}
|
||||
|
||||
export const getUsers = db =>
|
||||
//
|
||||
// Users
|
||||
//
|
||||
|
||||
export const getUsers = (db: DatabaseConnection) =>
|
||||
withDatabase(db, state => {
|
||||
return Object.values(state.users)
|
||||
})
|
||||
|
||||
export const getUser = (db, id) =>
|
||||
withDatabase(db, state => {
|
||||
export const getUser = (db: DatabaseConnection, id: string) =>
|
||||
withDatabase(db, (state: Database): User | null => {
|
||||
return state.users[id] ?? null
|
||||
})
|
||||
|
||||
// export const createUser = (db, { email, username }) =>
|
||||
// withDatabase(db, state => {
|
||||
// state.users[username] = {
|
||||
// email,
|
||||
// username,
|
||||
// }
|
||||
// })
|
||||
|
||||
// export const updateUser = (db, username, { email, password }) =>
|
||||
// withDatabase(db, state => {
|
||||
// state.users[username] = {
|
||||
// email,
|
||||
// password,
|
||||
// }
|
||||
// })
|
||||
|
||||
//
|
||||
// Problems
|
||||
//
|
||||
|
||||
export const createProblem = (db, { content, createdBy }) =>
|
||||
export const createProblem = (
|
||||
db: DatabaseConnection,
|
||||
{ content, createdBy }: Omit<Problem, 'id' | 'createdAt'>
|
||||
): Promise<ProblemId> =>
|
||||
withDatabase(db, state => {
|
||||
const nextId = (
|
||||
Object.keys(state.problems)
|
||||
.map(k => parseInt(k))
|
||||
.reduce((acc, id) => Math.max(acc, id)) + 1
|
||||
).toString()
|
||||
const nextId = (Object.keys(state.problems).length + 1).toString() as ProblemId
|
||||
|
||||
state.problems[nextId] = {
|
||||
id: nextId,
|
||||
@@ -130,12 +144,12 @@ export const createProblem = (db, { content, createdBy }) =>
|
||||
return nextId
|
||||
})
|
||||
|
||||
export const getProblem = (db, id) =>
|
||||
export const getProblem = (db: DatabaseConnection, id: string): Promise<Problem> =>
|
||||
withDatabase(db, state => {
|
||||
return state.problems[id]
|
||||
})
|
||||
|
||||
export const getProblems = db =>
|
||||
export const getProblems = (db: DatabaseConnection): Promise<Problem[]> =>
|
||||
withDatabase(db, state => {
|
||||
return Object.values(state.problems)
|
||||
})
|
||||
@@ -144,31 +158,52 @@ export const getProblems = db =>
|
||||
// Solutions
|
||||
//
|
||||
|
||||
export const createSolution = (db, { userId, problemId, content }) =>
|
||||
export const createSolution = (
|
||||
db: DatabaseConnection,
|
||||
{ sentBy, forProblem, content }: Omit<Solution, MetaProps>
|
||||
): Promise<SolutionId> =>
|
||||
withDatabase(db, state => {
|
||||
const id = crypto.randomBytes(10).toString('hex')
|
||||
state.solutions[id] = {
|
||||
id,
|
||||
userId,
|
||||
problemId,
|
||||
sentBy,
|
||||
forProblem,
|
||||
content,
|
||||
status: 'pending',
|
||||
}
|
||||
|
||||
return id
|
||||
})
|
||||
|
||||
export const getSolution = (db, id) =>
|
||||
export const getSolution = (db: DatabaseConnection, id: SolutionId): Promise<Solution> =>
|
||||
withDatabase(db, state => {
|
||||
return state.solutions[id]
|
||||
})
|
||||
|
||||
export const getSolutions = (db, { userId, problemId } = {}) =>
|
||||
export const updateSolution = (
|
||||
db: DatabaseConnection,
|
||||
id: SolutionId,
|
||||
solution: Omit<Solution, MetaProps>
|
||||
): Promise<Solution> =>
|
||||
withDatabase(db, state => {
|
||||
state.solutions[id] = { id, ...solution }
|
||||
return state.solutions[id]
|
||||
})
|
||||
|
||||
type SolutionsQuery = Partial<{
|
||||
sentBy: UserId
|
||||
forProblem: ProblemId
|
||||
}>
|
||||
|
||||
export const getSolutions = (db: DatabaseConnection, { sentBy, forProblem }: SolutionsQuery = {}) =>
|
||||
withDatabase(db, state => {
|
||||
let solutions = Object.values(state.solutions)
|
||||
|
||||
if (userId) {
|
||||
solutions = solutions.filter(s => s.userId === userId)
|
||||
if (sentBy) {
|
||||
solutions = solutions.filter(s => s.sentBy === sentBy)
|
||||
}
|
||||
if (problemId) {
|
||||
solutions = solutions.filter(s => s.problemId === problemId)
|
||||
if (forProblem) {
|
||||
solutions = solutions.filter(s => s.forProblem === forProblem)
|
||||
}
|
||||
|
||||
return solutions
|
||||
@@ -1,7 +0,0 @@
|
||||
export const initialDatabaseValue = {
|
||||
users: {
|
||||
['BachoSeven']: {},
|
||||
['aziis98']: {},
|
||||
},
|
||||
problems: {},
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { UserId } from '../../shared/model'
|
||||
import { Database } from './database'
|
||||
|
||||
export const initialDatabaseValue: Database = {
|
||||
users: {
|
||||
['BachoSeven']: {
|
||||
id: 'BachoSeven' as UserId,
|
||||
role: 'admin',
|
||||
},
|
||||
['aziis98']: {
|
||||
id: 'aziis98' as UserId,
|
||||
role: 'admin',
|
||||
},
|
||||
},
|
||||
problems: {},
|
||||
solutions: {},
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
const createRouter = setup => options => {
|
||||
const r = new Router()
|
||||
setup(r, options)
|
||||
return r
|
||||
}
|
||||
|
||||
export const createStatusRouter = createRouter(r => {
|
||||
r.get('/', (req, res) => {
|
||||
res.json({ url: req.originalUrl, status: 'ok' })
|
||||
})
|
||||
})
|
||||
|
||||
export class PingRouter extends Router {
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.post('/', (req, res) => {
|
||||
if (req.body?.message === 'ping') {
|
||||
res.json('pong')
|
||||
} else {
|
||||
res.status(400)
|
||||
res.json('Invalid request')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const INVALID_SESSION = `invalid session token`
|
||||
|
||||
export const authMiddleware = getUserForSession => async (req, res, next) => {
|
||||
if (req.cookies.sid) {
|
||||
const user = await getUserForSession(req.cookies.sid)
|
||||
if (user) {
|
||||
req.user = user
|
||||
} else {
|
||||
res.cookie('sid', '', { expires: new Date() })
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
export const authenticatedMiddleware = (req, res, next) => {
|
||||
req.user && next()
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import express, { NextFunction, Request, Response, Router } from 'express'
|
||||
import { User } from '../shared/model'
|
||||
|
||||
const createRouter =
|
||||
<T>(setup: (r: Router, options?: T) => void) =>
|
||||
(options: T) => {
|
||||
const r = express.Router()
|
||||
setup(r, options)
|
||||
return r
|
||||
}
|
||||
|
||||
export const createStatusRouter = createRouter(r => {
|
||||
r.get('/', (req, res) => {
|
||||
res.json({ url: req.originalUrl, status: 'ok' })
|
||||
})
|
||||
}) as () => Router
|
||||
|
||||
// export class PingRouter extends express.Router {
|
||||
// constructor() {
|
||||
// super()
|
||||
|
||||
// this.post('/', (req, res) => {
|
||||
// if (req.body?.message === 'ping') {
|
||||
// res.json('pong')
|
||||
// } else {
|
||||
// res.status(400)
|
||||
// res.json('Invalid request')
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
export const INVALID_SESSION = `invalid session token`
|
||||
|
||||
export const authMiddleware =
|
||||
<U>(getUserForSession: (sid: string) => U) =>
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (req.cookies.sid) {
|
||||
const user = await getUserForSession(req.cookies.sid)
|
||||
if (user) {
|
||||
// @ts-ignore
|
||||
req.user = user
|
||||
} else {
|
||||
res.cookie('sid', '', { expires: new Date() })
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user: User | null
|
||||
}
|
||||
|
||||
export const authenticatedMiddleware = (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
req.user && next()
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
import bodyParser from 'body-parser'
|
||||
import cookieParser from 'cookie-parser'
|
||||
|
||||
import express from 'express'
|
||||
|
||||
import { createStatusRouter, PingRouter } from './middlewares.js'
|
||||
import {
|
||||
createDatabase,
|
||||
createProblem,
|
||||
createSolution,
|
||||
getProblem,
|
||||
getProblems,
|
||||
getSolutions,
|
||||
getUser,
|
||||
getUsers,
|
||||
} from './db/database.js'
|
||||
import { initialDatabaseValue } from './db/example-data.js'
|
||||
|
||||
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)
|
||||
|
||||
async function getRequestUser(req) {
|
||||
const userId = sessions.getUserForSession(req.cookies.sid)
|
||||
if (!userId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const user = await getUser(db, userId)
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
const r = express.Router()
|
||||
|
||||
r.use(bodyParser.json())
|
||||
r.use(cookieParser())
|
||||
|
||||
r.use('/api/status', createStatusRouter())
|
||||
r.use('/api/ping', new PingRouter())
|
||||
|
||||
r.get('/api/current-user', async (req, res) => {
|
||||
res.json(await getRequestUser(req))
|
||||
})
|
||||
|
||||
r.post('/api/login', async (req, res) => {
|
||||
const { username } = req.body
|
||||
|
||||
const user = await getUser(db, username)
|
||||
if (!user) {
|
||||
res.sendStatus(403)
|
||||
return
|
||||
}
|
||||
|
||||
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/users', async (req, res) => {
|
||||
const requestUser = await getRequestUser(req)
|
||||
if (requestUser.role !== 'admin' && requestUser.role !== 'moderator') {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
const users = await getUsers(db)
|
||||
res.json(users)
|
||||
})
|
||||
|
||||
r.get('/api/problems', async (req, res) => {
|
||||
res.json(await getProblems(db))
|
||||
})
|
||||
|
||||
r.get('/api/problem/:id', async (req, res) => {
|
||||
res.json(await getProblem(db, req.params.id))
|
||||
})
|
||||
|
||||
r.post('/api/problem', async (req, res) => {
|
||||
const user = await getRequestUser(req)
|
||||
if (!user) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
if (user.role !== 'admin' && user.role !== 'moderator') {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
const id = await createProblem(db, {
|
||||
content: req.body.content,
|
||||
createBy: user.username,
|
||||
})
|
||||
|
||||
res.json(id)
|
||||
})
|
||||
|
||||
r.get('/api/solutions', async (req, res) => {
|
||||
let queryUserId = req.query.user
|
||||
let queryProblemId = req.query.problem
|
||||
|
||||
const requestUser = await getRequestUser(req)
|
||||
if (!requestUser) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
// if current user is not an administrator then force the user query to current user
|
||||
if (requestUser.role !== 'admin' && requestUser.role !== 'moderator') {
|
||||
queryUserId = requestUser.username
|
||||
}
|
||||
|
||||
res.json(await getSolutions(db, { userId: queryUserId, problemId: queryProblemId }))
|
||||
})
|
||||
|
||||
r.post('/api/solution', async (req, res) => {
|
||||
const user = await getRequestUser(req)
|
||||
if (!user) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
await createSolution(db, {
|
||||
userId: user.username,
|
||||
problemId: req.body.problemId,
|
||||
content: req.body.content,
|
||||
})
|
||||
|
||||
res.send({ status: 'ok' })
|
||||
})
|
||||
|
||||
r.get('/api/user/:id', async (req, res) => {
|
||||
const requestUser = await getRequestUser(req)
|
||||
if (!requestUser) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
if (requestUser.role !== 'admin' && requestUser.role !== 'moderator') {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
const requestedUser = await getUser(db, req.params.id)
|
||||
if (!requestedUser) {
|
||||
res.sendStatus(404)
|
||||
return
|
||||
}
|
||||
|
||||
res.json(requestedUser)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
import bodyParser from 'body-parser'
|
||||
import cookieParser from 'cookie-parser'
|
||||
|
||||
import express, { Request, Response, Router } from 'express'
|
||||
|
||||
import { createStatusRouter } from './middlewares'
|
||||
|
||||
import {
|
||||
createDatabase,
|
||||
createProblem,
|
||||
createSolution,
|
||||
getProblem,
|
||||
getProblems,
|
||||
getSolution,
|
||||
getSolutions,
|
||||
getUser,
|
||||
getUsers,
|
||||
updateSolution,
|
||||
} from './db/database'
|
||||
|
||||
import { isAdministrator, isStudent, Problem, ProblemId, UserId } from '../shared/model'
|
||||
import { initialDatabaseValue } from './db/example-data'
|
||||
|
||||
export async function createApiRouter() {
|
||||
type SessionId = string
|
||||
|
||||
const sessions = {
|
||||
store: {},
|
||||
createSession(userId: UserId) {
|
||||
const sid = crypto.randomBytes(10).toString('hex')
|
||||
this.store[sid] = userId
|
||||
return sid
|
||||
},
|
||||
getUserForSession(sid: SessionId) {
|
||||
return this.store[sid] ?? null
|
||||
},
|
||||
}
|
||||
|
||||
const db = createDatabase('./db.local.json', initialDatabaseValue)
|
||||
|
||||
async function getRequestUser(req: Request) {
|
||||
const userId = sessions.getUserForSession(req.cookies.sid)
|
||||
if (!userId) {
|
||||
return null
|
||||
}
|
||||
|
||||
console.log(userId)
|
||||
|
||||
return await getUser(db, userId)
|
||||
}
|
||||
|
||||
const r: Router = express.Router()
|
||||
|
||||
r.use(bodyParser.json())
|
||||
r.use(cookieParser())
|
||||
|
||||
r.use('/api/status', createStatusRouter())
|
||||
// r.use('/api/ping', new PingRouter())
|
||||
|
||||
r.get('/api/current-user', async (req, res) => {
|
||||
res.json(await getRequestUser(req))
|
||||
})
|
||||
|
||||
r.post('/api/login', async (req, res) => {
|
||||
const { id } = req.body
|
||||
|
||||
const user = await getUser(db, id)
|
||||
if (!user) {
|
||||
res.sendStatus(403)
|
||||
return
|
||||
}
|
||||
|
||||
res.cookie('sid', sessions.createSession(id), { 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/users', async (req, res) => {
|
||||
const requestUser = await getRequestUser(req)
|
||||
if (requestUser.role !== 'admin' && requestUser.role !== 'moderator') {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
const users = await getUsers(db)
|
||||
res.json(users)
|
||||
})
|
||||
|
||||
r.get('/api/problems', async (req, res) => {
|
||||
type ProblemWithSolutionsCount = Problem & { solutionsCount?: number }
|
||||
|
||||
const problems: ProblemWithSolutionsCount[] = await getProblems(db)
|
||||
const solutions = await getSolutions(db)
|
||||
|
||||
const solutionCounts: Record<ProblemId, number> = {}
|
||||
|
||||
for (const s of solutions) {
|
||||
solutionCounts[s.forProblem] ||= 0
|
||||
solutionCounts[s.forProblem]++
|
||||
}
|
||||
|
||||
for (const p of problems) {
|
||||
p.solutionsCount = solutionCounts[p.id] || 0
|
||||
}
|
||||
|
||||
res.json(problems)
|
||||
})
|
||||
|
||||
r.get('/api/problem/:id', async (req, res) => {
|
||||
res.json(await getProblem(db, req.params.id))
|
||||
})
|
||||
|
||||
r.post('/api/problem', async (req, res) => {
|
||||
const user = await getRequestUser(req)
|
||||
if (!user) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
if (user.role !== 'admin' && user.role !== 'moderator') {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
const id = await createProblem(db, {
|
||||
content: req.body.content,
|
||||
createdBy: user.id,
|
||||
})
|
||||
|
||||
res.json(id)
|
||||
})
|
||||
|
||||
r.get('/api/solution/:id', async (req, res) => {
|
||||
const user = await getRequestUser(req)
|
||||
|
||||
// l'utente deve essere loggato
|
||||
if (!user) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
const solution = await getSolution(db, req.params.id)
|
||||
|
||||
// uno studente che prova a ottenere la soluzione di un altro utente
|
||||
if (!isAdministrator(user.role) && solution.sentBy !== user.id) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
res.json(solution)
|
||||
})
|
||||
|
||||
r.get('/api/solutions', async (req, res) => {
|
||||
let queryUserId = req.query.user as string
|
||||
let queryProblemId = req.query.problem as string
|
||||
|
||||
const requestUser = await getRequestUser(req)
|
||||
if (!requestUser) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
// if current user is not an administrator then force the user query to current user
|
||||
if (!isAdministrator(requestUser.role)) {
|
||||
queryUserId = requestUser.id
|
||||
}
|
||||
|
||||
res.json(
|
||||
await getSolutions(db, {
|
||||
sentBy: queryUserId as UserId,
|
||||
forProblem: queryProblemId as ProblemId,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
r.post('/api/solution', async (req, res) => {
|
||||
const user = await getRequestUser(req)
|
||||
if (!user) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
await createSolution(db, {
|
||||
sentBy: user.id,
|
||||
forProblem: req.body.problemId,
|
||||
content: req.body.content,
|
||||
status: 'pending',
|
||||
})
|
||||
|
||||
res.send({ status: 'ok' })
|
||||
})
|
||||
|
||||
r.post('/api/solution/:id', async (req, res) => {
|
||||
const user = await getRequestUser(req)
|
||||
|
||||
// l'utente deve essere loggato
|
||||
if (!user) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
const solutionId = req.params.id
|
||||
const solution = await getSolution(db, solutionId)
|
||||
|
||||
// uno studente non può modificare una soluzione di un altro utente
|
||||
if (isStudent(user.role) && solution.sentBy !== user.id) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
// modifico la soluzione con il json mandato dal client nel body della richiesta
|
||||
await updateSolution(db, solutionId, req.body)
|
||||
|
||||
res.json({ status: 'ok' })
|
||||
})
|
||||
|
||||
r.get('/api/user/:id', async (req, res) => {
|
||||
const user = await getRequestUser(req)
|
||||
|
||||
// intanto l'utente deve essere loggato
|
||||
if (!user) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
// solo gli amministratori possono usare questa route
|
||||
if (!isAdministrator(user.role)) {
|
||||
res.sendStatus(401)
|
||||
return
|
||||
}
|
||||
|
||||
const requestedUser = await getUser(db, req.params.id)
|
||||
|
||||
// l'utente richiesto magari deve esistere
|
||||
if (!requestedUser) {
|
||||
res.sendStatus(404)
|
||||
return
|
||||
}
|
||||
|
||||
res.json(requestedUser)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ProblemId, SolutionId, UserId } from './model'
|
||||
|
||||
// export function refToUserId(ref: string): UserId {
|
||||
// if (!ref.startsWith('user/')) {
|
||||
// throw new Error(`"${ref}" is not a reference to a User`)
|
||||
// }
|
||||
|
||||
// return ref.slice('user/'.length)
|
||||
// }
|
||||
|
||||
// export function refToProblemId(ref: string): ProblemId {
|
||||
// if (!ref.startsWith('problem/')) {
|
||||
// throw new Error(`"${ref}" is not a reference to a Problem`)
|
||||
// }
|
||||
|
||||
// return ref.slice('problem/'.length)
|
||||
// }
|
||||
|
||||
// export function refToSolutionId(ref: string): SolutionId {
|
||||
// if (!ref.startsWith('solution/')) {
|
||||
// throw new Error(`"${ref}" is not a reference to a Solution`)
|
||||
// }
|
||||
|
||||
// return ref.slice('solution/'.length)
|
||||
// }
|
||||
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// Common
|
||||
//
|
||||
|
||||
export type CommonProps = 'id' | 'createdAt'
|
||||
|
||||
type Opaque<T, K, L extends string = 'opaque'> = T & { _: K; __: L }
|
||||
|
||||
type Id<T> = Opaque<string, T, 'id'>
|
||||
|
||||
//
|
||||
// Users
|
||||
//
|
||||
|
||||
export type AdministratorRole = 'admin' | 'moderator'
|
||||
export type StudentRole = 'student'
|
||||
|
||||
export type UserRole = AdministratorRole | StudentRole
|
||||
|
||||
export type UserId = Id<User>
|
||||
|
||||
export type User = {
|
||||
id: UserId
|
||||
role: UserRole
|
||||
}
|
||||
|
||||
export function isAdministrator(role: UserRole): role is AdministratorRole {
|
||||
return role !== 'student'
|
||||
}
|
||||
|
||||
export function isStudent(role: UserRole): role is StudentRole {
|
||||
return role === 'student'
|
||||
}
|
||||
|
||||
//
|
||||
// Problems
|
||||
//
|
||||
|
||||
export type ProblemId = Id<Problem>
|
||||
|
||||
export type Problem = {
|
||||
id: ProblemId
|
||||
content: string
|
||||
createdBy: UserId
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
//
|
||||
// Solutions
|
||||
//
|
||||
|
||||
export type SolutionStatus = 'pending' | 'rejected' | 'accepted'
|
||||
|
||||
export type SolutionId = string
|
||||
|
||||
export type Solution = {
|
||||
id: SolutionId
|
||||
sentBy: UserId
|
||||
forProblem: ProblemId
|
||||
content: string
|
||||
status: SolutionStatus
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export type RenderedPage = {
|
||||
html: string
|
||||
metadata: any
|
||||
}
|
||||
|
||||
export type RenderFunction = (url: string) => RenderedPage
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact"
|
||||
},
|
||||
"include": ["client", "shared"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"noImplicitAny": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["server", "shared"]
|
||||
}
|
||||
Reference in New Issue
Block a user