Refactoring to Typescript...
This commit is contained in:
@@ -1,14 +1,14 @@
|
|||||||
import Router from 'preact-router'
|
import Router from 'preact-router'
|
||||||
import { route } from 'preact-router'
|
import { route } from 'preact-router'
|
||||||
import { useEffect } from 'preact/hooks'
|
import { useEffect } from 'preact/hooks'
|
||||||
import { AdminPage } from './pages/Admin.jsx'
|
import { AdminPage } from './pages/Admin'
|
||||||
|
|
||||||
import { HomePage } from './pages/Home.jsx'
|
import { HomePage } from './pages/Home'
|
||||||
import { LoginPage } from './pages/Login.jsx'
|
import { LoginPage } from './pages/Login'
|
||||||
import { ProblemPage } from './pages/Problem.jsx'
|
import { ProblemPage } from './pages/Problem'
|
||||||
import { ProfilePage } from './pages/Profile.jsx'
|
import { ProfilePage } from './pages/Profile'
|
||||||
|
|
||||||
const Redirect = ({ to }) => {
|
const Redirect = ({ to }: { default: boolean; to: string }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
route(to, true)
|
route(to, true)
|
||||||
}, [])
|
}, [])
|
||||||
@@ -20,7 +20,7 @@ const Redirect = ({ to }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const App = ({ url }) => {
|
export const App = ({ url }: { url?: string }) => {
|
||||||
return (
|
return (
|
||||||
<Router url={url}>
|
<Router url={url}>
|
||||||
<HomePage path="/" />
|
<HomePage path="/" />
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Link } from 'preact-router/match'
|
import { Link } from 'preact-router/match'
|
||||||
|
import { isAdministrator, USER_ROLE_ADMIN, USER_ROLE_MODERATOR } from '../../shared/constants.js'
|
||||||
|
|
||||||
const ROLE_LABEL = {
|
const ROLE_LABEL = {
|
||||||
admin: 'Admin',
|
[USER_ROLE_ADMIN]: 'Admin',
|
||||||
moderator: 'Moderatore',
|
[USER_ROLE_MODERATOR]: 'Moderatore',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Header = ({ user, noLogin }) => (
|
export const Header = ({ user, noLogin }) => (
|
||||||
@@ -13,7 +14,7 @@ export const Header = ({ user, noLogin }) => (
|
|||||||
<nav>
|
<nav>
|
||||||
{user ? (
|
{user ? (
|
||||||
<>
|
<>
|
||||||
{user.role !== 'student' && (
|
{isAdministrator(user.role) && (
|
||||||
<div class="nav-item">
|
<div class="nav-item">
|
||||||
<Link activeClassName="active" href="/admin">
|
<Link activeClassName="active" href="/admin">
|
||||||
Pannello Admin
|
Pannello Admin
|
||||||
@@ -22,8 +23,8 @@ export const Header = ({ user, noLogin }) => (
|
|||||||
)}
|
)}
|
||||||
<div class="nav-item">
|
<div class="nav-item">
|
||||||
<Link activeClassName="active" href="/profile">
|
<Link activeClassName="active" href="/profile">
|
||||||
@{user.username}
|
@{user.id}
|
||||||
{user.role !== 'student' && <> ({ROLE_LABEL[user.role]})</>}
|
{isAdministrator(user.role) && <> ({ROLE_LABEL[user.role]})</>}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Markdown } from './Markdown.jsx'
|
import { Markdown } from './Markdown.jsx'
|
||||||
|
|
||||||
export const Problem = ({ id, content, createdBy }) => {
|
export const Problem = ({ id, content, solutionsCount }) => {
|
||||||
return (
|
return (
|
||||||
<div class="problem">
|
<div class="problem">
|
||||||
<div class="problem-header">
|
<div class="problem-header">
|
||||||
@@ -11,6 +11,11 @@ export const Problem = ({ id, content, createdBy }) => {
|
|||||||
<div class="problem-content">
|
<div class="problem-content">
|
||||||
<Markdown source={content} />
|
<Markdown source={content} />
|
||||||
</div>
|
</div>
|
||||||
|
{solutionsCount > 0 && (
|
||||||
|
<div class="problem-footer">
|
||||||
|
{solutionsCount} soluzion{solutionsCount === 1 ? 'e' : 'i'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import { refToUserId } from '../../shared/db-refs.js'
|
||||||
import { Markdown } from './Markdown.jsx'
|
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 (
|
return (
|
||||||
<div class="solution">
|
<div class="solution">
|
||||||
<div class="solution-header">
|
<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 renderToString from 'preact-render-to-string'
|
||||||
import { App } from './App.jsx'
|
// import { App } from './App'
|
||||||
import { MetadataContext } from './hooks.jsx'
|
import { MetadataContext } from './hooks'
|
||||||
|
|
||||||
export function render(url) {
|
import { RenderedPage } from '../shared/ssr'
|
||||||
|
|
||||||
|
export default (url: string): RenderedPage => {
|
||||||
const metadata = {}
|
const metadata = {}
|
||||||
|
|
||||||
const html = renderToString(
|
const html = renderToString(
|
||||||
<MetadataContext.Provider value={metadata}>
|
<MetadataContext.Provider value={metadata}>
|
||||||
<App url={url} />
|
{/* <App url={url} /> */}
|
||||||
</MetadataContext.Provider>
|
</MetadataContext.Provider>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { route } from 'preact-router'
|
import { route } from 'preact-router'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
|
import { isAdministrator, isStudent } from '../../shared/constants.js'
|
||||||
import { server } from '../api.jsx'
|
import { server } from '../api.jsx'
|
||||||
import { Header } from '../components/Header.jsx'
|
import { Header } from '../components/Header.jsx'
|
||||||
import { MarkdownEditor } from '../components/MarkdownEditor.jsx'
|
import { MarkdownEditor } from '../components/MarkdownEditor.jsx'
|
||||||
@@ -27,7 +28,7 @@ export const AdminPage = ({}) => {
|
|||||||
const [user] = useCurrentUser(user => {
|
const [user] = useCurrentUser(user => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
route('/login', true)
|
route('/login', true)
|
||||||
} else if (user.role !== 'admin' && user.role !== 'moderator') {
|
} else if (isStudent(user.role)) {
|
||||||
route('/', true)
|
route('/', true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -10,6 +10,8 @@ export const HomePage = () => {
|
|||||||
const [user] = useCurrentUser()
|
const [user] = useCurrentUser()
|
||||||
const [problems] = useReadResource('/api/problems', [])
|
const [problems] = useReadResource('/api/problems', [])
|
||||||
|
|
||||||
|
console.log(problems)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main class="page-home">
|
<main class="page-home">
|
||||||
<Header {...{ user }} />
|
<Header {...{ user }} />
|
||||||
@@ -13,7 +13,7 @@ export const LoginPage = () => {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
username,
|
id: username,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export const ProblemPage = ({ id }) => {
|
|||||||
|
|
||||||
const sendSolution = async () => {
|
const sendSolution = async () => {
|
||||||
await server.post('/api/solution', {
|
await server.post('/api/solution', {
|
||||||
problemId: id,
|
forProblem: id,
|
||||||
content: source,
|
content: source,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ export const ProfilePage = ({}) => {
|
|||||||
route('/login', true)
|
route('/login', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
setSolutions(await server.get(`/api/solutions?user=${user.username}`))
|
setSolutions(await server.get(`/api/solutions?user=${user.id}`))
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
@@ -27,8 +27,8 @@ export const ProfilePage = ({}) => {
|
|||||||
<Header {...{ user }} />
|
<Header {...{ user }} />
|
||||||
<div class="subtitle">Le tue soluzioni</div>
|
<div class="subtitle">Le tue soluzioni</div>
|
||||||
<div class="solution-list">
|
<div class="solution-list">
|
||||||
{solutions.map(({ problemId, content }) => (
|
{solutions.map(({ forProblem, content }) => (
|
||||||
<Solution {...{ problemId, content }} />
|
<Solution {...{ forProblem, content }} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div class="subtitle">Altro</div>
|
<div class="subtitle">Altro</div>
|
||||||
+16
-9
@@ -22,7 +22,7 @@ body {
|
|||||||
|
|
||||||
font-family: 'Lato', sans-serif;
|
font-family: 'Lato', sans-serif;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ input[type='text'] {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: 'Lato';
|
font-family: 'Lato';
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
color: #555;
|
color: #555;
|
||||||
|
|
||||||
border: 1px solid #c8c8c8;
|
border: 1px solid #c8c8c8;
|
||||||
@@ -102,7 +102,7 @@ input[type='text'] {
|
|||||||
|
|
||||||
font-family: 'Lato';
|
font-family: 'Lato';
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ button {
|
|||||||
|
|
||||||
font-family: 'Lato';
|
font-family: 'Lato';
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
color: #555;
|
color: #555;
|
||||||
|
|
||||||
border: 1px solid #c8c8c8;
|
border: 1px solid #c8c8c8;
|
||||||
@@ -199,7 +199,7 @@ $heading-scale: 1.25;
|
|||||||
.text-body {
|
.text-body {
|
||||||
font-family: 'EB Garamond', serif;
|
font-family: 'EB Garamond', serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 21px;
|
font-size: 18px;
|
||||||
|
|
||||||
line-height: 1.41;
|
line-height: 1.41;
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ main {
|
|||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: 24px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,6 +301,9 @@ header {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
@@ -327,7 +330,7 @@ header {
|
|||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr auto;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
.problem-header {
|
.problem-header {
|
||||||
@@ -336,7 +339,7 @@ header {
|
|||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|
||||||
.problem-title {
|
.problem-title {
|
||||||
font-size: 24px;
|
font-size: 22px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,6 +347,10 @@ header {
|
|||||||
.problem-content {
|
.problem-content {
|
||||||
@extend .text-body;
|
@extend .text-body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.problem-footer {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.solution {
|
.solution {
|
||||||
@@ -420,7 +427,7 @@ header {
|
|||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
font-family: 'DM Mono', monospace;
|
font-family: 'DM Mono', monospace;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
|
|
||||||
resize: none;
|
resize: none;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|||||||
+1
-1
@@ -30,6 +30,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- SSR OUTLET -->
|
<!-- SSR OUTLET -->
|
||||||
<script type="module" src="/client/entry-client.jsx"></script>
|
<script type="module" src="/client/entry-client.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+14
-6
@@ -5,11 +5,13 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "MODE=development node server.js",
|
"dev": "run-s build:server serve:dev",
|
||||||
"build": "run-s build:client build:server",
|
"build": "run-s build:client build:ssr build:server",
|
||||||
"build:client": "vite build --outDir dist/client",
|
"build:server": "esbuild server.ts --bundle --platform=node --format=esm --external:./node_modules/* --outdir=dist/server",
|
||||||
"build:server": "vite build --ssr client/entry-server.jsx --outDir dist/server",
|
"build:client": "vite build --outDir dist/entry-client",
|
||||||
"serve": "node server.js"
|
"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",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -35,7 +37,13 @@
|
|||||||
"vite": "^3.2.2"
|
"vite": "^3.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"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:
|
specifiers:
|
||||||
'@preact/preset-vite': ^2.4.0
|
'@preact/preset-vite': ^2.4.0
|
||||||
'@preact/signals': ^1.1.2
|
'@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
|
body-parser: ^1.20.1
|
||||||
chalk: ^5.1.2
|
chalk: ^5.1.2
|
||||||
concurrently: ^7.5.0
|
concurrently: ^7.5.0
|
||||||
cookie-parser: ^1.4.6
|
cookie-parser: ^1.4.6
|
||||||
|
esbuild: ^0.15.13
|
||||||
express: ^4.18.2
|
express: ^4.18.2
|
||||||
katex: ^0.16.3
|
katex: ^0.16.3
|
||||||
morgan: ^1.10.0
|
morgan: ^1.10.0
|
||||||
@@ -20,6 +25,7 @@ specifiers:
|
|||||||
remark-parse: ^10.0.1
|
remark-parse: ^10.0.1
|
||||||
remark-rehype: ^10.1.0
|
remark-rehype: ^10.1.0
|
||||||
sass: ^1.55.0
|
sass: ^1.55.0
|
||||||
|
typescript: ^4.8.4
|
||||||
unified: ^10.1.2
|
unified: ^10.1.2
|
||||||
url-pattern: ^1.0.3
|
url-pattern: ^1.0.3
|
||||||
vite: ^3.2.2
|
vite: ^3.2.2
|
||||||
@@ -47,8 +53,14 @@ dependencies:
|
|||||||
vite: 3.2.2_sass@1.56.0
|
vite: 3.2.2_sass@1.56.0
|
||||||
|
|
||||||
devDependencies:
|
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
|
concurrently: 7.5.0
|
||||||
|
esbuild: 0.15.13
|
||||||
npm-run-all: 4.1.5
|
npm-run-all: 4.1.5
|
||||||
|
typescript: 4.8.4
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -302,7 +314,6 @@ packages:
|
|||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [android]
|
os: [android]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/linux-loong64/0.15.13:
|
/@esbuild/linux-loong64/0.15.13:
|
||||||
@@ -311,7 +322,6 @@ packages:
|
|||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@jridgewell/gen-mapping/0.1.1:
|
/@jridgewell/gen-mapping/0.1.1:
|
||||||
@@ -426,12 +436,48 @@ packages:
|
|||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
dev: false
|
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:
|
/@types/debug/4.1.7:
|
||||||
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
|
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 0.7.31
|
'@types/ms': 0.7.31
|
||||||
dev: false
|
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:
|
/@types/hast/2.3.4:
|
||||||
resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==}
|
resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -448,14 +494,43 @@ packages:
|
|||||||
'@types/unist': 2.0.6
|
'@types/unist': 2.0.6
|
||||||
dev: false
|
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:
|
/@types/ms/0.7.31:
|
||||||
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
|
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/node/18.11.9:
|
||||||
|
resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/parse5/6.0.3:
|
/@types/parse5/6.0.3:
|
||||||
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
|
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
|
||||||
dev: false
|
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:
|
/@types/unist/2.0.6:
|
||||||
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
|
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -872,7 +947,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [android]
|
os: [android]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-android-arm64/0.15.13:
|
/esbuild-android-arm64/0.15.13:
|
||||||
@@ -881,7 +955,6 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-darwin-64/0.15.13:
|
/esbuild-darwin-64/0.15.13:
|
||||||
@@ -890,7 +963,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-darwin-arm64/0.15.13:
|
/esbuild-darwin-arm64/0.15.13:
|
||||||
@@ -899,7 +971,6 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-freebsd-64/0.15.13:
|
/esbuild-freebsd-64/0.15.13:
|
||||||
@@ -908,7 +979,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-freebsd-arm64/0.15.13:
|
/esbuild-freebsd-arm64/0.15.13:
|
||||||
@@ -917,7 +987,6 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-32/0.15.13:
|
/esbuild-linux-32/0.15.13:
|
||||||
@@ -926,7 +995,6 @@ packages:
|
|||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-64/0.15.13:
|
/esbuild-linux-64/0.15.13:
|
||||||
@@ -935,7 +1003,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-arm/0.15.13:
|
/esbuild-linux-arm/0.15.13:
|
||||||
@@ -944,7 +1011,6 @@ packages:
|
|||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-arm64/0.15.13:
|
/esbuild-linux-arm64/0.15.13:
|
||||||
@@ -953,7 +1019,6 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-mips64le/0.15.13:
|
/esbuild-linux-mips64le/0.15.13:
|
||||||
@@ -962,7 +1027,6 @@ packages:
|
|||||||
cpu: [mips64el]
|
cpu: [mips64el]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-ppc64le/0.15.13:
|
/esbuild-linux-ppc64le/0.15.13:
|
||||||
@@ -971,7 +1035,6 @@ packages:
|
|||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-riscv64/0.15.13:
|
/esbuild-linux-riscv64/0.15.13:
|
||||||
@@ -980,7 +1043,6 @@ packages:
|
|||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-linux-s390x/0.15.13:
|
/esbuild-linux-s390x/0.15.13:
|
||||||
@@ -989,7 +1051,6 @@ packages:
|
|||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-netbsd-64/0.15.13:
|
/esbuild-netbsd-64/0.15.13:
|
||||||
@@ -998,7 +1059,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [netbsd]
|
os: [netbsd]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-openbsd-64/0.15.13:
|
/esbuild-openbsd-64/0.15.13:
|
||||||
@@ -1007,7 +1067,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [openbsd]
|
os: [openbsd]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-sunos-64/0.15.13:
|
/esbuild-sunos-64/0.15.13:
|
||||||
@@ -1016,7 +1075,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [sunos]
|
os: [sunos]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-windows-32/0.15.13:
|
/esbuild-windows-32/0.15.13:
|
||||||
@@ -1025,7 +1083,6 @@ packages:
|
|||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-windows-64/0.15.13:
|
/esbuild-windows-64/0.15.13:
|
||||||
@@ -1034,7 +1091,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild-windows-arm64/0.15.13:
|
/esbuild-windows-arm64/0.15.13:
|
||||||
@@ -1043,7 +1099,6 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/esbuild/0.15.13:
|
/esbuild/0.15.13:
|
||||||
@@ -1074,7 +1129,6 @@ packages:
|
|||||||
esbuild-windows-32: 0.15.13
|
esbuild-windows-32: 0.15.13
|
||||||
esbuild-windows-64: 0.15.13
|
esbuild-windows-64: 0.15.13
|
||||||
esbuild-windows-arm64: 0.15.13
|
esbuild-windows-arm64: 0.15.13
|
||||||
dev: false
|
|
||||||
|
|
||||||
/escalade/3.1.1:
|
/escalade/3.1.1:
|
||||||
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
||||||
@@ -2489,6 +2543,12 @@ packages:
|
|||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
dev: false
|
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:
|
/unbox-primitive/1.0.2:
|
||||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import morgan from 'morgan'
|
|||||||
import { createServer as createViteServer } from 'vite'
|
import { createServer as createViteServer } from 'vite'
|
||||||
|
|
||||||
import { createApiRouter } from './server/routes.js'
|
import { createApiRouter } from './server/routes.js'
|
||||||
|
import { RenderFunction } from './shared/ssr.js'
|
||||||
|
|
||||||
const HTML_ROUTES = ['/', '/login', '/problem/:id', '/admin', '/profile']
|
const HTML_ROUTES = ['/', '/login', '/problem/:id', '/admin', '/profile']
|
||||||
|
|
||||||
@@ -46,7 +47,8 @@ async function createDevRouter() {
|
|||||||
const transformedTemplate = await vite.transformIndexHtml(req.originalUrl, indexHtml)
|
const transformedTemplate = await vite.transformIndexHtml(req.originalUrl, indexHtml)
|
||||||
|
|
||||||
// Load (to be bundled) entry point for server side rendering
|
// 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)
|
const { html, metadata } = render(req.originalUrl)
|
||||||
|
|
||||||
@@ -80,9 +82,10 @@ async function createDevRouter() {
|
|||||||
|
|
||||||
async function createProductionRouter() {
|
async function createProductionRouter() {
|
||||||
// Load bundled entry point for server side rendering
|
// 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'))
|
r.use('/', express.static('dist/client'))
|
||||||
|
|
||||||
@@ -2,21 +2,39 @@ import crypto from 'crypto'
|
|||||||
|
|
||||||
import { readFile, writeFile, access, constants } from 'fs/promises'
|
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
|
let flag = false
|
||||||
|
|
||||||
return (...args) => {
|
return ((...args: any) => {
|
||||||
if (flag) {
|
if (flag) {
|
||||||
throw new Error(message ?? `cannot run more than once`)
|
throw new Error(message ?? `cannot run more than once`)
|
||||||
}
|
}
|
||||||
flag = true
|
flag = true
|
||||||
return fn(...args)
|
return fn(...args)
|
||||||
}
|
}) as T
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMutex() {
|
interface Lock {
|
||||||
|
(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Mutex {
|
||||||
|
lock(): Promise<Lock>
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMutex(): Mutex {
|
||||||
let locked = false
|
let locked = false
|
||||||
const waiters = []
|
const waiters: ((fn: () => void) => void)[] = []
|
||||||
|
|
||||||
const unlock = () => {
|
const unlock = () => {
|
||||||
if (waiters.length > 0) {
|
if (waiters.length > 0) {
|
||||||
@@ -29,7 +47,7 @@ function createMutex() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lock = () => {
|
const lock = (): Promise<Lock> => {
|
||||||
if (locked) {
|
if (locked) {
|
||||||
console.log(`[Mutex] Putting into queue`)
|
console.log(`[Mutex] Putting into queue`)
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
@@ -38,20 +56,26 @@ function createMutex() {
|
|||||||
} else {
|
} else {
|
||||||
console.log(`[Mutex] Acquiring the lock`)
|
console.log(`[Mutex] Acquiring the lock`)
|
||||||
locked = true
|
locked = true
|
||||||
return once(unlock, `lock already released`)
|
return Promise.resolve(once(unlock, `lock already released`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { lock }
|
return { lock }
|
||||||
}
|
}
|
||||||
|
|
||||||
// l = createLock()
|
type DatabaseConnection = {
|
||||||
// unlock1 = await l.lock() // immediate
|
path: string
|
||||||
// unlock2 = await l.lock() // hangs...
|
initialValue: Database
|
||||||
// unlock1() // l2 restarts...
|
mu: Mutex
|
||||||
// unlock2() // no waiters so noop
|
}
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
path,
|
path,
|
||||||
initialValue,
|
initialValue,
|
||||||
@@ -59,8 +83,11 @@ export function createDatabase(path, initialValue) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function withDatabase({ path, initialValue, mu }, fn) {
|
async function withDatabase<R>(
|
||||||
const unlock = await mu.lock()
|
{ path, initialValue, mu }: DatabaseConnection,
|
||||||
|
fn: (db: Database) => R | Promise<R>
|
||||||
|
): Promise<R> {
|
||||||
|
const unlock: Lock = await mu.lock()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await access(path, constants.R_OK)
|
await access(path, constants.R_OK)
|
||||||
@@ -82,43 +109,30 @@ async function withDatabase({ path, initialValue, mu }, fn) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUsers = db =>
|
//
|
||||||
|
// Users
|
||||||
|
//
|
||||||
|
|
||||||
|
export const getUsers = (db: DatabaseConnection) =>
|
||||||
withDatabase(db, state => {
|
withDatabase(db, state => {
|
||||||
return Object.values(state.users)
|
return Object.values(state.users)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getUser = (db, id) =>
|
export const getUser = (db: DatabaseConnection, id: string) =>
|
||||||
withDatabase(db, state => {
|
withDatabase(db, (state: Database): User | null => {
|
||||||
return state.users[id] ?? 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
|
// Problems
|
||||||
//
|
//
|
||||||
|
|
||||||
export const createProblem = (db, { content, createdBy }) =>
|
export const createProblem = (
|
||||||
|
db: DatabaseConnection,
|
||||||
|
{ content, createdBy }: Omit<Problem, 'id' | 'createdAt'>
|
||||||
|
): Promise<ProblemId> =>
|
||||||
withDatabase(db, state => {
|
withDatabase(db, state => {
|
||||||
const nextId = (
|
const nextId = (Object.keys(state.problems).length + 1).toString() as ProblemId
|
||||||
Object.keys(state.problems)
|
|
||||||
.map(k => parseInt(k))
|
|
||||||
.reduce((acc, id) => Math.max(acc, id)) + 1
|
|
||||||
).toString()
|
|
||||||
|
|
||||||
state.problems[nextId] = {
|
state.problems[nextId] = {
|
||||||
id: nextId,
|
id: nextId,
|
||||||
@@ -130,12 +144,12 @@ export const createProblem = (db, { content, createdBy }) =>
|
|||||||
return nextId
|
return nextId
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getProblem = (db, id) =>
|
export const getProblem = (db: DatabaseConnection, id: string): Promise<Problem> =>
|
||||||
withDatabase(db, state => {
|
withDatabase(db, state => {
|
||||||
return state.problems[id]
|
return state.problems[id]
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getProblems = db =>
|
export const getProblems = (db: DatabaseConnection): Promise<Problem[]> =>
|
||||||
withDatabase(db, state => {
|
withDatabase(db, state => {
|
||||||
return Object.values(state.problems)
|
return Object.values(state.problems)
|
||||||
})
|
})
|
||||||
@@ -144,31 +158,52 @@ export const getProblems = db =>
|
|||||||
// Solutions
|
// Solutions
|
||||||
//
|
//
|
||||||
|
|
||||||
export const createSolution = (db, { userId, problemId, content }) =>
|
export const createSolution = (
|
||||||
|
db: DatabaseConnection,
|
||||||
|
{ sentBy, forProblem, content }: Omit<Solution, MetaProps>
|
||||||
|
): Promise<SolutionId> =>
|
||||||
withDatabase(db, state => {
|
withDatabase(db, state => {
|
||||||
const id = crypto.randomBytes(10).toString('hex')
|
const id = crypto.randomBytes(10).toString('hex')
|
||||||
state.solutions[id] = {
|
state.solutions[id] = {
|
||||||
id,
|
id,
|
||||||
userId,
|
sentBy,
|
||||||
problemId,
|
forProblem,
|
||||||
content,
|
content,
|
||||||
|
status: 'pending',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return id
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getSolution = (db, id) =>
|
export const getSolution = (db: DatabaseConnection, id: SolutionId): Promise<Solution> =>
|
||||||
withDatabase(db, state => {
|
withDatabase(db, state => {
|
||||||
return state.solutions[id]
|
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 => {
|
withDatabase(db, state => {
|
||||||
let solutions = Object.values(state.solutions)
|
let solutions = Object.values(state.solutions)
|
||||||
|
|
||||||
if (userId) {
|
if (sentBy) {
|
||||||
solutions = solutions.filter(s => s.userId === userId)
|
solutions = solutions.filter(s => s.sentBy === sentBy)
|
||||||
}
|
}
|
||||||
if (problemId) {
|
if (forProblem) {
|
||||||
solutions = solutions.filter(s => s.problemId === problemId)
|
solutions = solutions.filter(s => s.forProblem === forProblem)
|
||||||
}
|
}
|
||||||
|
|
||||||
return solutions
|
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