Compare commits

...

16 Commits
main ... dev

Author SHA1 Message Date
Francesco Minnocci 12687ba08c
Fix Dockerfile 3 years ago
Antonio De Lucreziis 1430c57423 Aggiunte un paio di cose al modello del db 3 years ago
Antonio De Lucreziis 1682cdca81 In alcuni punti go deduce da solo i tipi e ulteriori aggiunte a database-model.ts 3 years ago
Francesco Minnocci 7387307ad9
Merge branch 'dev' of git.phc.dm.unipi.it:phc/posti-dm into dev 3 years ago
Francesco Minnocci 6d4c742d4c
util/sets.go: Use generics Sets 3 years ago
Antonio De Lucreziis 3d10f70d71 Merge branch 'dev' of git.phc.dm.unipi.it:phc/posti-dm into dev 3 years ago
Antonio De Lucreziis 44ffecbdd2 Added a new typescript file describing the model of the new DB 3 years ago
Francesco Minnocci f670c78337
auth.go: make user of generics in RequestUser 3 years ago
Antonio De Lucreziis d5aa7c0ba2 chore: better makefile 3 years ago
Antonio De Lucreziis 2a76e0621b fix: added missing target files in vite.config.js and removed a compilation warning 3 years ago
Antonio De Lucreziis 35d42b8316 The generic zero function now is a bit more comprehensible 3 years ago
Antonio De Lucreziis 10704d946f Now server/auth is completely generic on the user type 3 years ago
Antonio De Lucreziis ff0e0a5dcc Preparing for generics 3 years ago
Antonio De Lucreziis 1495a5e45b Migliorata la struttura della libreria che gestisce le sessioni di autenticazione 3 years ago
Antonio De Lucreziis a970cea91a Aggiunte alcune delle nuove pagine 3 years ago
Antonio De Lucreziis fb2db34a9b Minor js riorganization and dev setup enhancements 3 years ago

@ -7,4 +7,4 @@ RUN make all
WORKDIR /app/dist WORKDIR /app/dist
CMD ["./posti-dm"] CMD ["./posti-dm"]
EXPOSE 3000 EXPOSE 4000

@ -18,8 +18,8 @@ build:
$(MAKE) -C server build $(MAKE) -C server build
$(MAKE) -C client build $(MAKE) -C client build
rsync -acvh server/bin/posti-dm dist/ rsync -ach server/bin/posti-dm dist/
rsync -acvh client/dist/ dist/ rsync -ach client/dist/ dist/
cp .env dist/.env cp .env dist/.env

@ -5,9 +5,9 @@ all: setup build
.PHONY: setup .PHONY: setup
setup: setup:
mkdir -p dist/ mkdir -p dist/
npm install
.PHONY: build .PHONY: build
build: build:
npm install
npm run build npm run build

@ -5,8 +5,6 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Posti DM</title> <title>Posti DM</title>
<script type="module" src="./src/index.js"></script>
</head> </head>
<body> <body>
<nav> <nav>
@ -15,18 +13,21 @@
</div> </div>
<div class="nav-group center"> <div class="nav-group center">
<div class="nav-item"> <div class="nav-item">
<a href=".">Posti DM</a> <a href="/stanze">Stanze</a>
</div>
<div class="nav-item">
<a href="/orari">Orari</a>
</div> </div>
</div> </div>
<div class="nav-group right"> <div id="nav-user" class="nav-group right">
<div id="login-label" class="nav-item"> <div class="nav-item login-label">
<a href="./login.html">Login</a> <a href="/login">Login</a>
</div> </div>
<div id="logged-label" class="nav-item hidden"> <div class="nav-item hidden logged-label">
<!-- @username --> <!-- @username -->
</div> </div>
<div id="logout-label" class="nav-item hidden"> <div class="nav-item hidden logout-label">
<button id="logout-button">Logout</button> <button class="logout-button">Logout</button>
</div> </div>
</div> </div>
</nav> </nav>

@ -5,8 +5,6 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Posti DM</title> <title>Posti DM</title>
<script type="module" src="./src/index.js"></script>
</head> </head>
<body> <body>
<nav> <nav>
@ -15,7 +13,7 @@
</div> </div>
<div class="nav-group center"> <div class="nav-group center">
<div class="nav-item"> <div class="nav-item">
<a href=".">Posti DM</a> <a href="/">Posti DM</a>
</div> </div>
</div> </div>
</nav> </nav>

@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Posti DM</title>
<link href="https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css" rel="stylesheet" />
</head>
<body>
<nav>
<div class="nav-group left">
<div class="nav-item" id="clock">14:23</div>
</div>
<div class="nav-group center">
<div class="nav-item">
<a href="/">Posti DM</a>
</div>
<div class="nav-item">
<a href="/stanze">Stanze</a>
</div>
</div>
<div class="nav-group right">
<div id="login-label" class="nav-item">
<a href="/login">Login</a>
</div>
<div id="logged-label" class="nav-item hidden">
<!-- @username -->
</div>
<div id="logout-label" class="nav-item hidden">
<button id="logout-button">Logout</button>
</div>
</div>
</nav>
<main>
<div id="orari-time-table" class="time-table">
<div class="controls">
<div class="current-range">Settimana, 21 Marzo &mdash; 27 Marzo</div>
<div class="group">
<div class="previous">
<button class="square">
<i class="bx bx-chevron-left bx-sm"></i>
</button>
</div>
<div class="today">
<button class="square">
<i class="bx bxs-home"></i>
</button>
</div>
<div class="next">
<button class="square">
<i class="bx bx-chevron-right bx-sm"></i>
</button>
</div>
</div>
</div>
<div class="weekly">
<div class="day">
<div class="title">Lun 21</div>
<div class="slot">
<div class="range">8:30 &ndash; 11:00</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">11:00 &ndash; 13:30</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">13:30 &ndash; 16:00</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">16:00 &ndash; 19:30</div>
<div class="counter">100/100</div>
</div>
</div>
<div class="day">
<div class="title">Mar 21</div>
<div class="slot">
<div class="range">8:30 &ndash; 11:00</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">11:00 &ndash; 13:30</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">13:30 &ndash; 16:00</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">16:00 &ndash; 19:30</div>
<div class="counter">100/100</div>
</div>
</div>
<div class="day">
<div class="title">Mer 21</div>
<div class="slot">
<div class="range">8:30 &ndash; 11:00</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">11:00 &ndash; 13:30</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">13:30 &ndash; 16:00</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">16:00 &ndash; 19:30</div>
<div class="counter">100/100</div>
</div>
</div>
<div class="day">
<div class="title">Gio 21</div>
<div class="slot">
<div class="range">8:30 &ndash; 11:00</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">11:00 &ndash; 13:30</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">13:30 &ndash; 16:00</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">16:00 &ndash; 19:30</div>
<div class="counter">100/100</div>
</div>
</div>
<div class="day">
<div class="title">Ven 21</div>
<div class="slot">
<div class="range">8:30 &ndash; 11:00</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">11:00 &ndash; 13:30</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">13:30 &ndash; 16:00</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">16:00 &ndash; 19:30</div>
<div class="counter">100/100</div>
</div>
</div>
<div class="day">
<div class="title">Sab 21</div>
<div class="slot">
<div class="range">8:30 &ndash; 11:00</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">11:00 &ndash; 13:30</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">13:30 &ndash; 16:00</div>
<div class="counter">100/100</div>
</div>
<div class="slot">
<div class="range">16:00 &ndash; 19:30</div>
<div class="counter">100/100</div>
</div>
</div>
</div>
</div>
</main>
<script type="module" src="./src/pages/orari.js"></script>
</body>
</html>

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Posti DM</title>
<link href="https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css" rel="stylesheet" />
</head>
<body>
<nav>
<div class="nav-group left">
<div class="nav-item" id="clock">14:23</div>
</div>
<div class="nav-group center">
<div class="nav-item">
<a href="/">Posti DM</a>
</div>
<div class="nav-item">
<a href="/stanze">Stanze</a>
</div>
<div class="nav-item">
<a href="/orari">Orari</a>
</div>
</div>
<div class="nav-group right">
<div id="login-label" class="nav-item">
<a href="/login">Login</a>
</div>
<div id="logged-label" class="nav-item hidden">
<!-- @username -->
</div>
<div id="logout-label" class="nav-item hidden">
<button id="logout-button">Logout</button>
</div>
</div>
</nav>
<main>
<p>Prenota</p>
</main>
<script type="module" src="./src/pages/prenota.js"></script>
</body>
</html>

@ -0,0 +1,117 @@
import './style.scss'
/**
* `BASE_URL` string from environment file without final slash (for more readable interpolated strings)
*/
export const BASE_URL = import.meta.env.BASE_URL.replace(/\/$/, '')
//
// User
//
const NotCached = Symbol('not cached')
let userSession = NotCached
export const User = {
async getLogged() {
if (userSession === NotCached) {
console.log('Caching user data...')
const res = await fetch(`${BASE_URL}/api/user`)
const user = await res.json()
userSession = user
}
return userSession
},
async login(username, password) {
try {
const response = await fetch(`${BASE_URL}/api/login`, {
method: 'POST',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
})
if (!response.ok) {
return { error: await response.text() }
}
// If successful redirect to homepage
location.href = `${BASE_URL}/`
} catch (e) {
return { error: e }
}
},
async logout() {
await fetch(`${BASE_URL}/api/logout`, { method: 'POST' })
location.href = `${BASE_URL}/`
},
}
//
// Room Event Source
//
export function createRoomEventStream(roomId) {
return new EventSource(`${BASE_URL}/api/room_events?id=${roomId}`)
}
//
// Database
//
export const Database = {
async getSeats(roomId) {
const seatList = await (await fetch(`${BASE_URL}/api/room/seats?id=${roomId}`)).json()
const seats = {}
seatList.forEach(seat => {
seats[seat.id] = seat
})
return seats
},
async occupySeat(seatId) {
const response = await fetch(`${BASE_URL}/api/seat/occupy?id=${seatId}`, {
method: 'POST',
})
if (!response.ok) {
throw new Error(await response.text())
}
await response.json()
},
async leaveSeat(seatId) {
const response = await fetch(`${BASE_URL}/api/seat/leave?id=${seatId}`, {
method: 'POST',
})
if (!response.ok) {
throw new Error(await response.text())
}
await response.json()
},
}
//
// Fix page links
//
// TODO: It might actually be better to just create a <page-link> custom element
// Always append BASE_URL to all <a> links in the page (in development mode links should point to html files)
document.querySelectorAll('a').forEach($a => {
const url = $a.getAttribute('href')
let newUrl = BASE_URL + url
if (import.meta.env.MODE === 'development') {
newUrl += newUrl.endsWith('/') ? 'index.html' : '.html'
}
$a.href = newUrl
})

@ -47,5 +47,5 @@ export function createGridLineCanvas($roomGrid) {
const render = () => renderGridLinesCanvas($canvas, [rows, cols]) const render = () => renderGridLinesCanvas($canvas, [rows, cols])
window.addEventListener('resize', render) window.addEventListener('resize', render)
render() requestAnimationFrame(render)
} }

@ -0,0 +1,40 @@
import { User } from '../common.js'
function elements(el) {
return {
elLoggedLabel: el.querySelector('.logged-label'),
elLoginLabel: el.querySelector('.login-label'),
elLogoutLabel: el.querySelector('.logout-label'),
elLogoutButton: el.querySelector('.logout-button'),
}
}
function setup(el) {
const { elLogoutButton } = elements(el)
elLogoutButton.addEventListener('click', () => User.logout())
}
function update(el, { user }) {
const { elLoggedLabel, elLoginLabel, elLogoutLabel } = elements(el)
elLoginLabel.classList.toggle('hidden', user)
const roleString =
user && user.permissions.length > 0 ? ` (${user.permissions.join(', ')})` : ''
elLoggedLabel.innerText = user ? `@${user.id}${roleString}` : ''
elLoggedLabel.classList.toggle('hidden', !user)
elLogoutLabel.classList.toggle('hidden', !user)
}
export function createNavUser(el) {
setup(el)
// Load current user
;(async () => {
const user = await User.getLogged()
update(el, { user })
})()
}

@ -1,8 +1,8 @@
import { BASE_URL, createRoomEventStream, Database, getLoggedUser } from '../index.js' import { BASE_URL, createRoomEventStream, Database, User } from '../common'
import { addTooltipElementListener, resetTooltip, setTooltipText } from './tooltip.js' import { addTooltipElementListener, setTooltipText } from './tooltip'
async function renderWidget(elSeatMap, seats) { async function renderWidget(elSeatMap, seats) {
const user = await getLoggedUser() const user = await User.getLogged()
Object.values(seats).forEach(seat => { Object.values(seats).forEach(seat => {
const { id, occupiedBy } = seat const { id, occupiedBy } = seat
@ -26,7 +26,7 @@ async function renderWidget(elSeatMap, seats) {
} }
export async function createSeatWidget($roomGrid, roomId) { export async function createSeatWidget($roomGrid, roomId) {
const user = await getLoggedUser() const user = await User.getLogged()
const elSeats = [...$roomGrid.querySelectorAll('[data-seat-id]')] const elSeats = [...$roomGrid.querySelectorAll('[data-seat-id]')]
const elSeatMap = {} const elSeatMap = {}

@ -0,0 +1,3 @@
import { BASE_URL, Database } from '../common'
export function createTimeTable(el) {}

@ -1,52 +0,0 @@
import './style.scss'
export const BASE_URL = import.meta.env.BASE_URL.replace(/\/$/, '')
let USER = false
export async function getLoggedUser() {
if (USER === false) {
console.log('Caching user data...')
USER = await (await fetch(`${BASE_URL}/api/user`)).json()
}
return USER
}
export function createRoomEventStream(roomId) {
return new EventSource(`${BASE_URL}/api/room_events?id=${roomId}`)
}
export const Database = {
async getSeats(roomId) {
const seatList = await (await fetch(`${BASE_URL}/api/room/seats?id=${roomId}`)).json()
const seats = {}
seatList.forEach(seat => {
seats[seat.id] = seat
})
return seats
},
async occupySeat(seatId) {
const response = await fetch(`${BASE_URL}/api/seat/occupy?id=${seatId}`, {
method: 'POST',
})
if (!response.ok) {
throw new Error(await response.text())
}
await response.json()
},
async leaveSeat(seatId) {
const response = await fetch(`${BASE_URL}/api/seat/leave?id=${seatId}`, {
method: 'POST',
})
if (!response.ok) {
throw new Error(await response.text())
}
await response.json()
},
}

@ -1,48 +1,30 @@
import { createGridLineCanvas } from '../components/gridlines.js' import '../common'
import { BASE_URL, getLoggedUser } from '../index.js'
import { createSeatWidget } from '../components/seats-widget.js'
import { createClock } from '../components/clock.js'
import { attachTooltip } from '../components/tooltip.js'
const elClock = document.querySelector('#clock') import { createGridLineCanvas } from '../components/grid-lines'
import { BASE_URL } from '../common'
import { createSeatWidget } from '../components/seats-widget'
import { createClock } from '../components/clock'
import { attachTooltip } from '../components/tooltip'
import { createNavUser } from '../components/nav-user.js'
const elLoggedLabel = document.querySelector('#logged-label') const elClock = document.querySelector('#clock')
const elLoginLabel = document.querySelector('#login-label')
const elLogoutLabel = document.querySelector('#logout-label')
const elLogoutButton = document.querySelector('#logout-button') const elNavUser = document.querySelector('#nav-user')
const elRoomGrid = document.querySelector('.room-grid') const elRoomGrid = document.querySelector('.room-grid')
async function logout() {
await fetch(`${BASE_URL}/api/logout`, { method: 'POST' })
location.href = `${BASE_URL}`
}
async function main() { async function main() {
const urlSearchParams = new URLSearchParams(window.location.search) const urlSearchParams = new URLSearchParams(window.location.search)
const params = Object.fromEntries(urlSearchParams.entries()) const params = Object.fromEntries(urlSearchParams.entries())
console.log(params) console.log(params)
elLogoutButton.addEventListener('click', () => logout())
const user = await getLoggedUser()
if (user) {
elLoginLabel.classList.add('hidden')
elLoggedLabel.innerText =
'@' + user.id + (user.permissions.length > 0 ? ` (${user.permissions.join(', ')})` : '')
elLoggedLabel.classList.remove('hidden')
elLogoutLabel.classList.remove('hidden')
}
// Widgets // Widgets
// createClock(elClock) // createClock(elClock)
createGridLineCanvas(elRoomGrid) createGridLineCanvas(elRoomGrid)
createSeatWidget(elRoomGrid, 'aula-stud') createSeatWidget(elRoomGrid, 'aula-stud')
createNavUser(elNavUser)
// Use tooltips only on desktop // Use tooltips only on desktop
if (matchMedia('(pointer: fine)').matches) { if (matchMedia('(pointer: fine)').matches) {
attachTooltip() attachTooltip()

@ -1,4 +1,6 @@
import { BASE_URL, getLoggedUser } from '../index.js' import '../common'
import { BASE_URL, User } from '../common'
// //
// Page Element // Page Element
@ -16,8 +18,7 @@ const elLoginButton = document.querySelector('#button-login')
// //
let errorTimeoutHandle = null let errorTimeoutHandle = null
function displayFormErrorMessage(e) {
function displayErrorString(e) {
if (errorTimeoutHandle) clearTimeout(errorTimeoutHandle) if (errorTimeoutHandle) clearTimeout(errorTimeoutHandle)
elErrorString.classList.toggle('hidden', false) elErrorString.classList.toggle('hidden', false)
elErrorString.innerText = e.toString() elErrorString.innerText = e.toString()
@ -31,28 +32,10 @@ function displayErrorString(e) {
// Handle Login Button // Handle Login Button
// //
async function login() { async function onLoginButtonClick() {
try { const res = await User.login(elLoginUsernameInput.value, elLoginPasswordInput.value)
const response = await fetch(`${BASE_URL}/api/login`, { if (res.error) {
method: 'POST', displayFormErrorMessage(res.error)
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: elLoginUsernameInput.value,
password: elLoginPasswordInput.value,
}),
})
if (!response.ok) {
displayErrorString(await response.text())
return
}
location.href = `${BASE_URL}/`
} catch (e) {
displayErrorString(e)
} }
} }
@ -61,16 +44,17 @@ async function login() {
// //
async function main() { async function main() {
const user = await getLoggedUser() const user = await User.getLogged()
console.log(user)
if (user) { if (user) {
location.href = `${BASE_URL}/` location.href = `${BASE_URL}/`
} }
elLoginButton.addEventListener('click', () => login()) elLoginButton.addEventListener('click', () => {
onLoginButtonClick()
})
elLoginPasswordInput.addEventListener('keydown', e => { elLoginPasswordInput.addEventListener('keydown', e => {
if (e.key === 'Enter') login() if (e.key === 'Enter') onLoginButtonClick()
}) })
elLoginUsernameInput.focus() elLoginUsernameInput.focus()

@ -0,0 +1,10 @@
import '../common'
import { createTimeTable } from '../components/time-table.js'
const elTimeTable = document.querySelector('#orari-time-table')
async function main() {
createTimeTable(elTimeTable)
}
main()

@ -0,0 +1,7 @@
import '../common.js'
async function main() {
// TODO: ...
}
main()

@ -102,18 +102,37 @@ a:visited {
} }
} }
@mixin button-panel {
background: #ddd;
color: #222;
box-shadow: 0 2px 12px 2px #0002, 0 0 2px 0px #0005;
&:hover {
background: #d0d0d0;
}
}
button { button {
@include button-panel;
border: none; border: none;
background: #ddd;
padding: 0 0.75rem; padding: 0 0.75rem;
height: 2rem; height: 2rem;
border-radius: 3px; border-radius: 3px;
color: #222;
box-shadow: 0 2px 12px 2px #0002, 0 1px 3px 0px #0003;
cursor: pointer; cursor: pointer;
&:hover { display: flex;
background: #d0d0d0; align-items: center;
justify-content: center;
&:active {
background: #c4c4c4;
}
&.square {
padding: 0;
width: 2rem;
height: 2rem;
} }
} }
@ -197,7 +216,7 @@ main {
width: 100%; width: 100%;
@media screen and (min-width: $media-small-device-size) { @media screen and (min-width: $media-small-device-size) {
max-width: 75ch; max-width: 80ch;
} }
padding-top: 2rem; padding-top: 2rem;
@ -349,6 +368,76 @@ main {
} }
} }
//
// Time Table
//
.time-table {
@include padded-panel;
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
.controls {
display: flex;
width: 100%;
gap: 1rem;
justify-content: space-between;
.group {
display: flex;
gap: 1rem;
}
align-items: center;
}
.weekly {
display: flex;
width: 100%;
gap: 0.75rem;
.day {
display: flex;
flex-direction: column;
width: 100%;
gap: 0.5rem;
.title {
display: flex;
flex-direction: column;
align-items: center;
font-weight: 600;
}
.slot {
@include button-panel;
width: 100%;
min-height: 5rem;
padding: 0.25rem;
font-size: 13px;
user-select: none;
cursor: pointer;
.counter {
font-size: 18px;
}
}
}
}
}
//
// Tooltip
//
body { body {
position: relative; position: relative;

@ -2,17 +2,19 @@ import { defineConfig, loadEnv } from 'vite'
import { resolve } from 'path' import { resolve } from 'path'
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
// Load environment variables with no prefixes
process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') } process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }
console.log(`BASE_URL = "${process.env.BASE_URL}"`)
return { return {
base: process.env.BASE_URL, base: process.env.BASE_URL,
css: { preprocessorOptions: { scss: { charset: false } } },
build: { build: {
rollupOptions: { rollupOptions: {
input: { input: {
main: resolve(__dirname, 'index.html'), main: resolve(__dirname, 'index.html'),
login: resolve(__dirname, 'login.html'), login: resolve(__dirname, 'login.html'),
orari: resolve(__dirname, 'orari.html'),
prenota: resolve(__dirname, 'prenota.html'),
}, },
}, },
}, },

@ -0,0 +1,111 @@
//
// Prelude
//
type Natural = number
type Integer = number
type Rational = [number, number]
type Real = number
type Datetime = string
type Time = string
type Maybe<T> = { present: false } | { present: true; value: T }
//
// PostiDM
//
type UserID = string
type BookingID = string
type SlotID = string
type SeatID = string
type RoomID = string
type UserPermission = 'basic' | 'helper' | 'moderator' | 'admin'
type PostiDM = {
users: Map<UserID, User>
rooms: Map<RoomID, Room>
seats: Map<SeatID, Seat>
slots: Map<SlotID, Slot>
getCurrentWeekSlots(): Slot[]
bookings: Map<BookingID, Booking>
}
// TODO: Tutte le funzioni "getCurrentSomething()" in realtà sono da pensare meglio in modo da poter passare un range temporale o qualcosa del genere
/**
* Un utente loggato con credenziali di ateneo
*/
type User = {
id: UserID
permissions: Set<UserPermission>
getBookings(): Booking[]
getCurrentSeat(): Maybe<Seat>
}
/**
* Una prenotazione di un utente per un certo slot orario
*/
type Booking = {
id: BookingID
timestamp: Datetime
slotID: SlotID
userID: UserID // cioè ok c'è User.bookings però pensando in SQL è sempre meglio avere comunque un id qui (forse)
getSlot(): Slot // giusto per comodità per parlare direttamente dell'oggetto "Slot" relativo ad un "Booking"
getSeat(): Seat // ".getSlot().getSeat()"
}
/**
* Slot rappresenta uno slot orario prenotabile per un certo posto
*/
type Slot = {
id: SlotID
seatID: SeatID
// Magari invece di eliminare uno slot, in quanto ricrearlo potrebbe essere complicato e forse è meglio che i moderatori semplicemente disabilitino uno slot se vogliono non renderlo prenotabile (?)
// disabled: boolean
range: {
from: Datetime
to: Datetime
}
getCurrentBooking(): Maybe<Booking>
}
/**
* Seat rappresenta un posto in dipartimento in una certa stanza
*/
type Seat = {
id: SeatID
diagram: {
x: Natural
y: Natural
width: Natural
height: Natural
}
// TODO: Forse per ora è meglio fare che più utenti possono prenotare lo stesso posto così inizialmente possiamo fare che in ogni stanza c'è solo un posto e la gente può prenotarsi solo "alla stanza" e non al posto specifico, altrimenti facciamo "getCurrentBookedUser(): Maybe<User>"
getCurrentlyBookedUsers(): User[]
getRoom(): Room
}
/**
* Room rappresenta una stanza in dipartimento e contiene dei posti
*/
type Room = {
id: RoomID
name: string
diagram: {
gridRows: Natural
gridCols: Natural
}
getSeats(): Seat[]
getTotalSeatCount(): Natural // ".seatIDs.length"
getCurrentBookedSeatCount(): Natural
}

@ -0,0 +1,83 @@
package main
import (
"fmt"
"log"
"net/http"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/auth"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/db"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/util"
)
// simpleAuthenticator holds an in memory map of session tokens and a reference to the main database interface
type simpleAuthenticator struct {
// sessions is a map from a sessionToken to userID
sessions map[string]string
database db.Database
}
func (service *simpleAuthenticator) CheckUserPassword(userID, password string) error {
if password != "phc" {
return fmt.Errorf(`invalid password`)
}
// FIXME: al momento quando la password è giusta creiamo tutti gli account necessari
err := service.database.CreateUser(&db.User{
ID: userID,
Permissions: make(util.Set[string]),
})
if err != nil {
log.Printf(`got "%v" while trying to log as @%s`, err, userID)
return nil
}
return nil
}
func (service *simpleAuthenticator) UserPermissions(userID string) ([]string, error) {
user, err := service.database.GetUser(userID)
if err != nil {
return nil, err
}
return user.Permissions.Elements(), nil
}
func (service *simpleAuthenticator) SessionTokenFromUser(userID string) (string, error) {
user, err := service.database.GetUser(userID)
if err != nil {
return "", err
}
token := util.RandomHash(20)
service.sessions[token] = user.ID
return token, nil
}
func (service *simpleAuthenticator) UserFromSessionToken(session string) (*db.User, error) {
userID, present := service.sessions[session]
if !present {
return nil, auth.ErrNoUserForSession
}
user, err := service.database.GetUser(userID)
if err != nil {
return nil, err
}
return user, nil
}
func (service *simpleAuthenticator) AuthenticationFailed(err error) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusUnauthorized)
})
}
func (service *simpleAuthenticator) OtherError(err error) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError)
})
}

@ -13,119 +13,124 @@ var (
ErrNoUserForSession = errors.New(`no user for session token`) ErrNoUserForSession = errors.New(`no user for session token`)
) )
var SessionCookieName = "session" // TODO: Make configurable type User[IdType any] interface {
UID() IdType
// AuthMiddlewareConfig configures the middleware to only accept logged users (if "RequireLogged" is true) and with certain permissions (user must have all permissions inside "WithPermissions")
type AuthMiddlewareConfig struct {
// RequireLogged rejects not logged users if true
RequireLogged bool
// WithPermissions is the list of permissions the user should have to pass the middleware
WithPermissions []string
} }
// // Authenticator is the spec of this library // Authenticator should be used by clients to provide authentication functions and mapping of session tokens to users
// type Authenticator interface { type Authenticator[UserID any, U User[UserID]] interface {
// // Login checks user credentials and adds a session cookie to the user if successfull
// Login(w http.ResponseWriter, r *http.Request, userID, password string)
// // Logout clears the user session cookies (by setting the session cookie timeout to 0)
// Logout(w http.ResponseWriter)
// // Middleware is a configurable middleware to authenticate http routes based on logged status and permissions
// Middleware(*AuthMiddlewareConfig) func(http.Handler) http.Handler
// // LoggedMiddleware accepts all logged users without checking for specific permissions
// LoggedMiddleware() func(http.Handler) http.Handler
// // RequestUser returns the userID for this cookie session token
// RequestUser(r *http.Request) (string, error)
// }
// var _ Authenticator = &AuthService{}
// AuthService handles cookies, authentication and authorization of http routes by providing middlewares, logint/logout methods, user sessions and retriving the userID of an authenticated request.
type AuthService struct {
// CheckUserPassword is called to login a user and create a corresponding session, see also "SessionTokenFromUser" // CheckUserPassword is called to login a user and create a corresponding session, see also "SessionTokenFromUser"
CheckUserPassword func(userID string, password string) error CheckUserPassword(user UserID, password string) error
// GetUserPermissions gets the list of permissions for this user // GetUserPermissions gets the list of permissions for this user
UserPermissions func(userID string) ([]string, error) UserPermissions(user UserID) ([]string, error)
// SessionTokenFromUser returns a (new) session token that represents this user // SessionTokenFromUser returns a (new) session token that represents this user
SessionTokenFromUser func(userID string) (string, error) SessionTokenFromUser(user UserID) (string, error)
// UserFromSessionToken returns the corresponing user for this session token or "auth.ErrNoUserForSession" // UserFromSessionToken returns the corresponding user for this session token or "service.ErrNoUserForSession"
UserFromSessionToken func(session string) (string, error) UserFromSessionToken(session string) (*U, error)
// AuthenticationFailed handles failed authentications // AuthenticationFailed handles failed authentications
AuthenticationFailed func(error) http.Handler AuthenticationFailed(error) http.Handler
// OtherError handles other errors // OtherError handles other errors
OtherError func(error) http.Handler OtherError(error) http.Handler
}
// MiddlewareConfig configures the middleware to only accept logged users (if "RequireLogged" is true) and with certain permissions (user must have all permissions inside "NeedPermissions")
type MiddlewareConfig struct {
// RequireLogged rejects not logged users if true
RequireLogged bool
// NeedPermissions is the list of permissions the user should have to pass the middleware
NeedPermissions []string
}
// AuthSessionService given an Authenticator provides functions to login and logout users and an http.Handler middleware that accept users based on permissions and login status
type AuthSessionService[UserID any, U User[UserID]] struct {
SessionCookieName string
SessionCookiePath string
SessionCookieDuration time.Duration
Authenticator Authenticator[UserID, U]
}
// NewAuthSessionService creates a new "*AuthSessionService" with a default session cookie name and path
func NewAuthSessionService[UserID any, U User[UserID]](auth Authenticator[UserID, U]) *AuthSessionService[UserID, U] {
oneWeek := 7 * 24 * time.Hour
return &AuthSessionService[UserID, U]{
"session",
"/",
oneWeek,
auth,
}
} }
// Login tries to login a user given its id and password // Login tries to login a user given its id and password
func (auth *AuthService) Login(w http.ResponseWriter, r *http.Request, userID, password string) error { func (service *AuthSessionService[UserID, U]) Login(w http.ResponseWriter, userID UserID, password string) error {
if err := auth.CheckUserPassword(userID, password); err != nil { if err := service.Authenticator.CheckUserPassword(userID, password); err != nil {
return err return err
} }
token, err := auth.SessionTokenFromUser(userID) token, err := service.Authenticator.SessionTokenFromUser(userID)
if err != nil { if err != nil {
return err return err
} }
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: SessionCookieName, Name: service.SessionCookieName,
Path: "/", // TODO: Make configurable Path: service.SessionCookiePath,
Value: token, Value: token,
Expires: time.Now().Add(7 * 24 * time.Hour), // TODO: Make configurable Expires: time.Now().Add(service.SessionCookieDuration),
}) })
return nil return nil
} }
// Logout clears the session cookie from a request effectivly logging out the user for future requests // Logout clears the session cookie from a request effectively logging out the user for future requests
func (auth *AuthService) Logout(w http.ResponseWriter) { func (service *AuthSessionService[UserID, U]) Logout(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: SessionCookieName, Name: service.SessionCookieName,
Path: "/", Path: service.SessionCookiePath,
Value: "", Value: "",
Expires: time.Now(), Expires: time.Now(),
}) })
} }
// Middleware checks if the user is logged or not and if the user has all the permissions set in "config.WithPermissions" // Middleware returns an http middleware that accepts users based on login status and permissions
func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Handler) http.Handler { func (service *AuthSessionService[UserID, U]) Middleware(config *MiddlewareConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(SessionCookieName) cookie, err := r.Cookie(service.SessionCookieName)
if err == http.ErrNoCookie { if err == http.ErrNoCookie {
if !config.RequireLogged { // Login not required if !config.RequireLogged { // Login not required
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
auth.AuthenticationFailed(err).ServeHTTP(w, r) service.Authenticator.AuthenticationFailed(err).ServeHTTP(w, r)
return return
} }
if err != nil { if err != nil {
auth.OtherError(err).ServeHTTP(w, r) service.Authenticator.OtherError(err).ServeHTTP(w, r)
return return
} }
userID, err := auth.UserFromSessionToken(cookie.Value) user, err := service.Authenticator.UserFromSessionToken(cookie.Value)
if err == ErrNoUserForSession { if err == ErrNoUserForSession {
auth.Logout(w) service.Logout(w)
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
return return
} }
if err != nil { if err != nil {
auth.OtherError(err).ServeHTTP(w, r) service.Authenticator.OtherError(err).ServeHTTP(w, r)
return return
} }
if config.RequireLogged { if config.RequireLogged {
userPerms, err := auth.UserPermissions(userID) userPerms, err := service.Authenticator.UserPermissions((*user).UID())
if err != nil { if err != nil {
auth.OtherError(err).ServeHTTP(w, r) service.Authenticator.OtherError(err).ServeHTTP(w, r)
return return
} }
@ -136,7 +141,7 @@ func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Hand
// check the user has all the permissions to access the route // check the user has all the permissions to access the route
hasAll := true hasAll := true
for _, perm := range config.WithPermissions { for _, perm := range config.NeedPermissions {
if _, present := userPermsMap[perm]; !present { if _, present := userPermsMap[perm]; !present {
hasAll = false hasAll = false
break break
@ -144,14 +149,14 @@ func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Hand
} }
if !hasAll { if !hasAll {
auth.AuthenticationFailed(err).ServeHTTP(w, r) service.Authenticator.AuthenticationFailed(err).ServeHTTP(w, r)
return return
} }
} }
// Refresh session cookie expiration // Refresh session cookie expiration
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: SessionCookieName, Name: service.SessionCookieName,
Path: "/", Path: "/",
Value: cookie.Value, Value: cookie.Value,
Expires: time.Now().Add(7 * 24 * time.Hour), Expires: time.Now().Add(7 * 24 * time.Hour),
@ -166,30 +171,25 @@ func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Hand
// //
// Middleware(*AuthMiddlewareConfig) // Middleware(*AuthMiddlewareConfig)
// //
// that checks if a user is logged, no extra permissions are checked // that only accepts logged in users, no special permissions are checked
func (auth *AuthService) LoggedMiddleware() func(http.Handler) http.Handler { func (service *AuthSessionService[UserID, U]) LoggedMiddleware() func(http.Handler) http.Handler {
return auth.Middleware(&AuthMiddlewareConfig{ return service.Middleware(&MiddlewareConfig{
RequireLogged: true, RequireLogged: true,
WithPermissions: []string{}, NeedPermissions: []string{},
}) })
} }
// RequestUser retrives the "userID" from the given request based on the cookie session token. // RequestUser retrieves the "userID" from the given request based on the cookie session token.
// When generics arrive this will become something like func (service *AuthSessionService[UserID, U]) RequestUser(r *http.Request) (*U, error) {
// cookie, err := r.Cookie(service.SessionCookieName)
// func (auth *AuthService[U]) RequestUser(r *http.Request) *U
//
// and will just return nil if no user is present.
func (auth *AuthService) RequestUser(r *http.Request) (string, error) {
cookie, err := r.Cookie(SessionCookieName)
if err != nil { if err != nil {
return "", err return nil, err
} }
userID, err := auth.UserFromSessionToken(cookie.Value) user, err := service.Authenticator.UserFromSessionToken(cookie.Value)
if err == ErrNoUserForSession { if err == ErrNoUserForSession {
return "", err return nil, err
} }
return userID, nil return user, nil
} }

@ -33,11 +33,15 @@ const (
// User represents a user in the database. // User represents a user in the database.
type User struct { type User struct {
ID string `json:"id"` ID string `json:"id"`
Permissions util.StringSet `json:"permissions"` Permissions util.Set[string] `json:"permissions"`
// passwordSaltHash string // passwordSaltHash string
} }
func (user User) UID() string {
return user.ID
}
// Room represents a room in the database, a room has an id, a name and a collection of seatIDs of this room. // Room represents a room in the database, a room has an id, a name and a collection of seatIDs of this room.
type Room struct { type Room struct {
ID string `json:"id"` ID string `json:"id"`
@ -96,8 +100,8 @@ type Seat struct {
// Database Interfaces // Database Interfaces
// //
// Store is the main interface for interacting with database implementations, for now the only implementation is "memDB". // Database is the main interface for interacting with database implementations, for now the only implementation is "memDB".
type Store interface { type Database interface {
BeginTransaction() BeginTransaction()
EndTransaction() EndTransaction()
@ -118,7 +122,7 @@ type Store interface {
GetRoomOccupiedSeats(roomID string) ([]string, error) GetRoomOccupiedSeats(roomID string) ([]string, error)
GetRoomFreeSeats(roomID string) ([]string, error) GetRoomFreeSeats(roomID string) ([]string, error)
GetUserSeats(userID string) (util.StringSet, error) GetUserSeats(userID string) (util.Set[string], error)
OccupySeat(seatID, userID string) error OccupySeat(seatID, userID string) error
FreeSeat(seatID string) error FreeSeat(seatID string) error
@ -127,7 +131,7 @@ type Store interface {
// TODO: Create an SQLite implementation // TODO: Create an SQLite implementation
// NewInMemoryStore creates an instance of memDB hidden behind the Store interface. // NewInMemoryStore creates an instance of memDB hidden behind the Store interface.
func NewInMemoryStore() Store { func NewInMemoryStore() Database {
db := &memDB{ db := &memDB{
&sync.Mutex{}, &sync.Mutex{},
@ -157,12 +161,12 @@ func NewInMemoryStore() Store {
db.users["aziis98"] = &User{ db.users["aziis98"] = &User{
ID: "aziis98", ID: "aziis98",
Permissions: util.NewStringSet(PermissionAdmin), Permissions: util.NewSet(PermissionAdmin),
} }
db.users["bachoseven"] = &User{ db.users["bachoseven"] = &User{
ID: "bachoseven", ID: "bachoseven",
Permissions: util.NewStringSet(PermissionAdmin), Permissions: util.NewSet(PermissionAdmin),
} }
return db return db

@ -146,14 +146,14 @@ func (db *memDB) GetRoomFreeSeats(roomID string) ([]string, error) {
return seats, nil return seats, nil
} }
func (db *memDB) GetUserSeats(userID string) (util.StringSet, error) { func (db *memDB) GetUserSeats(userID string) (util.Set[string], error) {
for _, seat := range db.seats { for _, seat := range db.seats {
if len(seat.OccupiedBy) > 0 && seat.OccupiedBy[0] == userID { if len(seat.OccupiedBy) > 0 && seat.OccupiedBy[0] == userID {
return util.NewStringSet(seat.ID), nil return util.NewSet(seat.ID), nil
} }
} }
return util.NewStringSet(), nil return util.NewSet[string](), nil
} }
func (db *memDB) OccupySeat(seatID, userID string) error { func (db *memDB) OccupySeat(seatID, userID string) error {

@ -2,88 +2,35 @@ package main
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/auth" "git.phc.dm.unipi.it/aziis98/posti-dm/server/auth"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/db" "git.phc.dm.unipi.it/aziis98/posti-dm/server/db"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/httputil" "git.phc.dm.unipi.it/aziis98/posti-dm/server/httputil"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/httputil/serverevents" "git.phc.dm.unipi.it/aziis98/posti-dm/server/httputil/serverevents"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/util"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
type Server struct { type Server struct {
authService *auth.AuthService auth *auth.AuthSessionService[string, db.User]
roomServerEventStreams map[string]serverevents.Handler roomServerEventStreams map[string]serverevents.Handler
Database db.Store Database db.Database
ApiRoute chi.Router ApiRoute chi.Router
} }
func NewServer() *Server { func NewServer() *Server {
sessions := make(map[string]*db.User)
database := db.NewInMemoryStore() database := db.NewInMemoryStore()
server := &Server{ server := &Server{
Database: database, Database: database,
authService: &auth.AuthService{ auth: auth.NewAuthSessionService[string, db.User](
CheckUserPassword: func(userID, password string) error { &simpleAuthenticator{
if password != "phc" { make(map[string]string),
return fmt.Errorf(`invalid password`) database,
}
// FIXME: al momento quando la password è giusta creiamo tutti gli account necessari
err := database.CreateUser(&db.User{
ID: userID,
Permissions: make(util.StringSet),
})
if err != nil {
log.Printf(`got "%v" while trying to log as @%s`, err, userID)
return nil
}
return nil
},
UserPermissions: func(userID string) ([]string, error) {
user, err := database.GetUser(userID)
if err != nil {
return nil, err
}
return user.Permissions.Elements(), nil
},
SessionTokenFromUser: func(userID string) (string, error) {
user, err := database.GetUser(userID)
if err != nil {
return "", err
}
token := util.RandomHash(20)
sessions[token] = user
return token, nil
},
UserFromSessionToken: func(session string) (string, error) {
user, present := sessions[session]
if !present {
return "", auth.ErrNoUserForSession
}
return user.ID, nil
},
AuthenticationFailed: func(err error) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusUnauthorized)
})
},
OtherError: func(err error) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError)
})
},
}, },
),
roomServerEventStreams: make(map[string]serverevents.Handler), roomServerEventStreams: make(map[string]serverevents.Handler),
@ -103,7 +50,7 @@ func NewServer() *Server {
func (server *Server) setupRoutes() { func (server *Server) setupRoutes() {
api := server.ApiRoute api := server.ApiRoute
database := server.Database database := server.Database
auth := server.authService auth := server.auth
// FIXME: in realtà tutte le routes che interagiscono con il db dovrebbero essere in transazione con // FIXME: in realtà tutte le routes che interagiscono con il db dovrebbero essere in transazione con
// database.BeginTransaction() // database.BeginTransaction()
@ -122,7 +69,7 @@ func (server *Server) setupRoutes() {
return return
} }
err := auth.Login(w, r, credentials.Username, credentials.Password) err := auth.Login(w, credentials.Username, credentials.Password)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized) http.Error(w, err.Error(), http.StatusUnauthorized)
return return
@ -138,17 +85,12 @@ func (server *Server) setupRoutes() {
}) })
api.Get("/user", func(w http.ResponseWriter, r *http.Request) { api.Get("/user", func(w http.ResponseWriter, r *http.Request) {
userID, err := auth.RequestUser(r) user, err := auth.RequestUser(r)
if err != nil { if err != nil {
httputil.WriteJSON(w, nil) httputil.WriteJSON(w, nil)
return return
} }
user, err := database.GetUser(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
}
httputil.WriteJSON(w, user) httputil.WriteJSON(w, user)
}) })
@ -188,38 +130,40 @@ func (server *Server) setupRoutes() {
server.roomServerEventStreams[roomID[0]].ServeHTTP(w, r) server.roomServerEventStreams[roomID[0]].ServeHTTP(w, r)
}) })
requestSeat := func(r *http.Request) (*db.Seat, error) {
seatID, ok := r.URL.Query()["id"]
if !ok {
return nil, fmt.Errorf(`missing seat id`)
}
seat, err := database.GetSeat(seatID[0])
if err != nil {
return nil, err
}
return seat, nil
}
api.With(auth.LoggedMiddleware()). api.With(auth.LoggedMiddleware()).
Post("/seat/occupy", func(w http.ResponseWriter, r *http.Request) { Post("/seat/occupy", func(w http.ResponseWriter, r *http.Request) {
database.BeginTransaction() database.BeginTransaction()
defer database.EndTransaction() defer database.EndTransaction()
userID, err := auth.RequestUser(r) user, err := auth.RequestUser(r)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized) http.Error(w, err.Error(), http.StatusUnauthorized)
return return
} }
user, err := database.GetUser(userID) seat, err := requestSeat(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
seatID, ok := r.URL.Query()["id"]
if !ok {
http.Error(w, `missing seat id`, http.StatusBadRequest)
return
}
seat, err := database.GetSeat(seatID[0])
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
// a seat can be occupied only if empty; simple users can occupy a seat only if they have occupied no other seat; admins and moderator (for now) can occupy even more than one seat (as occupied by an anonymous?) to fix the free seat count of the room // a seat can be occupied only if empty; simple users can occupy a seat only if they have occupied no other seat; admins and moderator (for now) can occupy even more than one seat (as occupied by an anonymous?) to fix the free seat count of the room
if len(seat.OccupiedBy) == 0 { if len(seat.OccupiedBy) == 0 {
userSeats, err := database.GetUserSeats(userID) userSeats, err := database.GetUserSeats(user.ID)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -230,7 +174,7 @@ func (server *Server) setupRoutes() {
return return
} }
if err := database.OccupySeat(seatID[0], userID); err != nil { if err := database.OccupySeat(seat.ID, user.ID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
@ -244,34 +188,22 @@ func (server *Server) setupRoutes() {
api.With(auth.LoggedMiddleware()). api.With(auth.LoggedMiddleware()).
Post("/seat/leave", func(w http.ResponseWriter, r *http.Request) { Post("/seat/leave", func(w http.ResponseWriter, r *http.Request) {
userID, err := auth.RequestUser(r) user, err := auth.RequestUser(r)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized) http.Error(w, err.Error(), http.StatusUnauthorized)
return return
} }
user, err := database.GetUser(userID) seat, err := requestSeat(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
seatID, ok := r.URL.Query()["id"]
if !ok {
http.Error(w, `missing seat id`, http.StatusBadRequest)
return
}
seat, err := database.GetSeat(seatID[0])
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
// Check permissions, if the place is occupied then only its owner, a moderator or an admin can clear it // Check permissions, if the place is occupied then only its owner, a moderator or an admin can clear it
if len(seat.OccupiedBy) > 0 { if len(seat.OccupiedBy) > 0 {
if user.ID == seat.OccupiedBy[0] || user.Permissions.HasAny(db.PermissionAdmin, db.PermissionModerator) { if user.ID == seat.OccupiedBy[0] || user.Permissions.HasAny(db.PermissionAdmin, db.PermissionModerator) {
if err := database.FreeSeat(seatID[0]); err != nil { if err := database.FreeSeat(seat.ID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }

@ -4,12 +4,10 @@ import "encoding/json"
type present struct{} type present struct{}
// Waiting for Go 18... type Set[T comparable] map[T]present
// type Set[T comparable] map[T]present
type StringSet map[string]present
func NewStringSet(elements ...string) StringSet { func NewSet[T comparable](elements ...T) Set[T] {
set := StringSet{} set := Set[T]{}
for _, elem := range elements { for _, elem := range elements {
set.Add(elem) set.Add(elem)
@ -18,12 +16,12 @@ func NewStringSet(elements ...string) StringSet {
return set return set
} }
func (set StringSet) Has(value string) bool { func (set Set[T]) Has(value T) bool {
_, present := set[value] _, present := set[value]
return present return present
} }
func (set StringSet) HasAny(values ...string) bool { func (set Set[T]) HasAny(values ...T) bool {
for _, value := range values { for _, value := range values {
if _, present := set[value]; present { if _, present := set[value]; present {
return true return true
@ -32,7 +30,7 @@ func (set StringSet) HasAny(values ...string) bool {
return false return false
} }
func (set StringSet) Contains(other StringSet) bool { func (set Set[T]) Contains(other Set[T]) bool {
for v := range other { for v := range other {
if !set.Has(v) { if !set.Has(v) {
return false return false
@ -41,16 +39,16 @@ func (set StringSet) Contains(other StringSet) bool {
return true return true
} }
func (set StringSet) Add(value string) { func (set Set[T]) Add(value T) {
set[value] = present{} set[value] = present{}
} }
func (set StringSet) Remove(value string) { func (set Set[T]) Remove(value T) {
delete(set, value) delete(set, value)
} }
func (set StringSet) Elements() []string { func (set Set[T]) Elements() []T {
elements := []string{} elements := []T{}
for elem := range set { for elem := range set {
elements = append(elements, elem) elements = append(elements, elem)
@ -59,12 +57,12 @@ func (set StringSet) Elements() []string {
return elements return elements
} }
func (set StringSet) MarshalJSON() ([]byte, error) { func (set Set[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(set.Elements()) return json.Marshal(set.Elements())
} }
func (set StringSet) UnmarshalJSON(data []byte) error { func (set Set[T]) UnmarshalJSON(data []byte) error {
elements := []string{} elements := []T{}
if err := json.Unmarshal(data, &elements); err != nil { if err := json.Unmarshal(data, &elements); err != nil {
return err return err
} }

Loading…
Cancel
Save