Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12687ba08c
|
||
|
|
1430c57423 | ||
|
|
1682cdca81 | ||
|
|
7387307ad9
|
||
|
|
6d4c742d4c
|
||
|
|
3d10f70d71 | ||
|
|
44ffecbdd2 | ||
|
|
f670c78337
|
||
|
|
d5aa7c0ba2 | ||
|
|
2a76e0621b | ||
|
|
35d42b8316 | ||
|
|
10704d946f | ||
|
|
ff0e0a5dcc | ||
|
|
1495a5e45b | ||
|
|
a970cea91a | ||
|
|
fb2db34a9b |
+1
-1
@@ -7,4 +7,4 @@ RUN make all
|
||||
|
||||
WORKDIR /app/dist
|
||||
CMD ["./posti-dm"]
|
||||
EXPOSE 3000
|
||||
EXPOSE 4000
|
||||
|
||||
@@ -18,8 +18,8 @@ build:
|
||||
$(MAKE) -C server build
|
||||
$(MAKE) -C client build
|
||||
|
||||
rsync -acvh server/bin/posti-dm dist/
|
||||
rsync -acvh client/dist/ dist/
|
||||
rsync -ach server/bin/posti-dm dist/
|
||||
rsync -ach client/dist/ dist/
|
||||
|
||||
cp .env dist/.env
|
||||
|
||||
|
||||
+1
-1
@@ -5,9 +5,9 @@ all: setup build
|
||||
.PHONY: setup
|
||||
setup:
|
||||
mkdir -p dist/
|
||||
npm install
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
|
||||
+10
-9
@@ -5,8 +5,6 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Posti DM</title>
|
||||
|
||||
<script type="module" src="./src/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
@@ -15,18 +13,21 @@
|
||||
</div>
|
||||
<div class="nav-group center">
|
||||
<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 class="nav-group right">
|
||||
<div id="login-label" class="nav-item">
|
||||
<a href="./login.html">Login</a>
|
||||
<div id="nav-user" class="nav-group right">
|
||||
<div class="nav-item login-label">
|
||||
<a href="/login">Login</a>
|
||||
</div>
|
||||
<div id="logged-label" class="nav-item hidden">
|
||||
<div class="nav-item hidden logged-label">
|
||||
<!-- @username -->
|
||||
</div>
|
||||
<div id="logout-label" class="nav-item hidden">
|
||||
<button id="logout-button">Logout</button>
|
||||
<div class="nav-item hidden logout-label">
|
||||
<button class="logout-button">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
+1
-3
@@ -5,8 +5,6 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Posti DM</title>
|
||||
|
||||
<script type="module" src="./src/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
@@ -15,7 +13,7 @@
|
||||
</div>
|
||||
<div class="nav-group center">
|
||||
<div class="nav-item">
|
||||
<a href=".">Posti DM</a>
|
||||
<a href="/">Posti DM</a>
|
||||
</div>
|
||||
</div>
|
||||
</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 — 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 – 11:00</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">11:00 – 13:30</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">13:30 – 16:00</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">16:00 – 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 – 11:00</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">11:00 – 13:30</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">13:30 – 16:00</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">16:00 – 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 – 11:00</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">11:00 – 13:30</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">13:30 – 16:00</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">16:00 – 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 – 11:00</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">11:00 – 13:30</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">13:30 – 16:00</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">16:00 – 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 – 11:00</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">11:00 – 13:30</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">13:30 – 16:00</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">16:00 – 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 – 11:00</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">11:00 – 13:30</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">13:30 – 16:00</div>
|
||||
<div class="counter">100/100</div>
|
||||
</div>
|
||||
<div class="slot">
|
||||
<div class="range">16:00 – 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])
|
||||
|
||||
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 { addTooltipElementListener, resetTooltip, setTooltipText } from './tooltip.js'
|
||||
import { BASE_URL, createRoomEventStream, Database, User } from '../common'
|
||||
import { addTooltipElementListener, setTooltipText } from './tooltip'
|
||||
|
||||
async function renderWidget(elSeatMap, seats) {
|
||||
const user = await getLoggedUser()
|
||||
const user = await User.getLogged()
|
||||
|
||||
Object.values(seats).forEach(seat => {
|
||||
const { id, occupiedBy } = seat
|
||||
@@ -26,7 +26,7 @@ async function renderWidget(elSeatMap, seats) {
|
||||
}
|
||||
|
||||
export async function createSeatWidget($roomGrid, roomId) {
|
||||
const user = await getLoggedUser()
|
||||
const user = await User.getLogged()
|
||||
|
||||
const elSeats = [...$roomGrid.querySelectorAll('[data-seat-id]')]
|
||||
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()
|
||||
},
|
||||
}
|
||||
+11
-29
@@ -1,48 +1,30 @@
|
||||
import { createGridLineCanvas } from '../components/gridlines.js'
|
||||
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'
|
||||
import '../common'
|
||||
|
||||
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 elClock = document.querySelector('#clock')
|
||||
|
||||
const elLoggedLabel = document.querySelector('#logged-label')
|
||||
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')
|
||||
|
||||
async function logout() {
|
||||
await fetch(`${BASE_URL}/api/logout`, { method: 'POST' })
|
||||
location.href = `${BASE_URL}`
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
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
|
||||
// createClock(elClock)
|
||||
createGridLineCanvas(elRoomGrid)
|
||||
createSeatWidget(elRoomGrid, 'aula-stud')
|
||||
|
||||
createNavUser(elNavUser)
|
||||
|
||||
// Use tooltips only on desktop
|
||||
if (matchMedia('(pointer: fine)').matches) {
|
||||
attachTooltip()
|
||||
|
||||
+13
-29
@@ -1,4 +1,6 @@
|
||||
import { BASE_URL, getLoggedUser } from '../index.js'
|
||||
import '../common'
|
||||
|
||||
import { BASE_URL, User } from '../common'
|
||||
|
||||
//
|
||||
// Page Element
|
||||
@@ -16,8 +18,7 @@ const elLoginButton = document.querySelector('#button-login')
|
||||
//
|
||||
|
||||
let errorTimeoutHandle = null
|
||||
|
||||
function displayErrorString(e) {
|
||||
function displayFormErrorMessage(e) {
|
||||
if (errorTimeoutHandle) clearTimeout(errorTimeoutHandle)
|
||||
elErrorString.classList.toggle('hidden', false)
|
||||
elErrorString.innerText = e.toString()
|
||||
@@ -31,28 +32,10 @@ function displayErrorString(e) {
|
||||
// Handle Login Button
|
||||
//
|
||||
|
||||
async function login() {
|
||||
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: elLoginUsernameInput.value,
|
||||
password: elLoginPasswordInput.value,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
displayErrorString(await response.text())
|
||||
return
|
||||
}
|
||||
|
||||
location.href = `${BASE_URL}/`
|
||||
} catch (e) {
|
||||
displayErrorString(e)
|
||||
async function onLoginButtonClick() {
|
||||
const res = await User.login(elLoginUsernameInput.value, elLoginPasswordInput.value)
|
||||
if (res.error) {
|
||||
displayFormErrorMessage(res.error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,16 +44,17 @@ async function login() {
|
||||
//
|
||||
|
||||
async function main() {
|
||||
const user = await getLoggedUser()
|
||||
console.log(user)
|
||||
const user = await User.getLogged()
|
||||
if (user) {
|
||||
location.href = `${BASE_URL}/`
|
||||
}
|
||||
|
||||
elLoginButton.addEventListener('click', () => login())
|
||||
elLoginButton.addEventListener('click', () => {
|
||||
onLoginButtonClick()
|
||||
})
|
||||
|
||||
elLoginPasswordInput.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') login()
|
||||
if (e.key === 'Enter') onLoginButtonClick()
|
||||
})
|
||||
|
||||
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()
|
||||
+97
-8
@@ -102,21 +102,40 @@ a:visited {
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
@mixin button-panel {
|
||||
background: #ddd;
|
||||
padding: 0 0.75rem;
|
||||
height: 2rem;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
box-shadow: 0 2px 12px 2px #0002, 0 1px 3px 0px #0003;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 12px 2px #0002, 0 0 2px 0px #0005;
|
||||
|
||||
&:hover {
|
||||
background: #d0d0d0;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@include button-panel;
|
||||
|
||||
border: none;
|
||||
padding: 0 0.75rem;
|
||||
height: 2rem;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:active {
|
||||
background: #c4c4c4;
|
||||
}
|
||||
|
||||
&.square {
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
input[type='password'],
|
||||
textarea {
|
||||
@@ -197,7 +216,7 @@ main {
|
||||
width: 100%;
|
||||
|
||||
@media screen and (min-width: $media-small-device-size) {
|
||||
max-width: 75ch;
|
||||
max-width: 80ch;
|
||||
}
|
||||
|
||||
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 {
|
||||
position: relative;
|
||||
|
||||
|
||||
@@ -2,17 +2,19 @@ import { defineConfig, loadEnv } from 'vite'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
// Load environment variables with no prefixes
|
||||
process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') }
|
||||
|
||||
console.log(`BASE_URL = "${process.env.BASE_URL}"`)
|
||||
|
||||
return {
|
||||
base: process.env.BASE_URL,
|
||||
css: { preprocessorOptions: { scss: { charset: false } } },
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(__dirname, 'index.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)
|
||||
})
|
||||
}
|
||||
+76
-76
@@ -13,119 +13,124 @@ var (
|
||||
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 {
|
||||
// Authenticator should be used by clients to provide authentication functions and mapping of session tokens to users
|
||||
type Authenticator[UserID any, U User[UserID]] interface {
|
||||
// CheckUserPassword is called to login a user and create a corresponding session, see also "SessionTokenFromUser"
|
||||
CheckUserPassword(user UserID, password string) error
|
||||
|
||||
// GetUserPermissions gets the list of permissions for this user
|
||||
UserPermissions(user UserID) ([]string, error)
|
||||
|
||||
// SessionTokenFromUser returns a (new) session token that represents this user
|
||||
SessionTokenFromUser(user UserID) (string, error)
|
||||
// UserFromSessionToken returns the corresponding user for this session token or "service.ErrNoUserForSession"
|
||||
UserFromSessionToken(session string) (*U, error)
|
||||
|
||||
// AuthenticationFailed handles failed authentications
|
||||
AuthenticationFailed(error) http.Handler
|
||||
// OtherError handles other errors
|
||||
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
|
||||
|
||||
// WithPermissions is the list of permissions the user should have to pass the middleware
|
||||
WithPermissions []string
|
||||
// NeedPermissions is the list of permissions the user should have to pass the middleware
|
||||
NeedPermissions []string
|
||||
}
|
||||
|
||||
// // Authenticator is the spec of this library
|
||||
// type Authenticator 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)
|
||||
// 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
|
||||
|
||||
// // 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
|
||||
Authenticator Authenticator[UserID, U]
|
||||
}
|
||||
|
||||
// // RequestUser returns the userID for this cookie session token
|
||||
// RequestUser(r *http.Request) (string, error)
|
||||
// }
|
||||
// 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
|
||||
|
||||
// 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 func(userID string, password string) error
|
||||
|
||||
// GetUserPermissions gets the list of permissions for this user
|
||||
UserPermissions func(userID string) ([]string, error)
|
||||
|
||||
// SessionTokenFromUser returns a (new) session token that represents this user
|
||||
SessionTokenFromUser func(userID string) (string, error)
|
||||
// UserFromSessionToken returns the corresponing user for this session token or "auth.ErrNoUserForSession"
|
||||
UserFromSessionToken func(session string) (string, error)
|
||||
|
||||
// AuthenticationFailed handles failed authentications
|
||||
AuthenticationFailed func(error) http.Handler
|
||||
// OtherError handles other errors
|
||||
OtherError func(error) http.Handler
|
||||
return &AuthSessionService[UserID, U]{
|
||||
"session",
|
||||
"/",
|
||||
oneWeek,
|
||||
auth,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if err := auth.CheckUserPassword(userID, password); err != nil {
|
||||
func (service *AuthSessionService[UserID, U]) Login(w http.ResponseWriter, userID UserID, password string) error {
|
||||
if err := service.Authenticator.CheckUserPassword(userID, password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := auth.SessionTokenFromUser(userID)
|
||||
token, err := service.Authenticator.SessionTokenFromUser(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: SessionCookieName,
|
||||
Path: "/", // TODO: Make configurable
|
||||
Name: service.SessionCookieName,
|
||||
Path: service.SessionCookiePath,
|
||||
Value: token,
|
||||
Expires: time.Now().Add(7 * 24 * time.Hour), // TODO: Make configurable
|
||||
Expires: time.Now().Add(service.SessionCookieDuration),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout clears the session cookie from a request effectivly logging out the user for future requests
|
||||
func (auth *AuthService) Logout(w http.ResponseWriter) {
|
||||
// Logout clears the session cookie from a request effectively logging out the user for future requests
|
||||
func (service *AuthSessionService[UserID, U]) Logout(w http.ResponseWriter) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: SessionCookieName,
|
||||
Path: "/",
|
||||
Name: service.SessionCookieName,
|
||||
Path: service.SessionCookiePath,
|
||||
Value: "",
|
||||
Expires: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// Middleware checks if the user is logged or not and if the user has all the permissions set in "config.WithPermissions"
|
||||
func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Handler) http.Handler {
|
||||
// Middleware returns an http middleware that accepts users based on login status and permissions
|
||||
func (service *AuthSessionService[UserID, U]) Middleware(config *MiddlewareConfig) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
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 !config.RequireLogged { // Login not required
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
auth.AuthenticationFailed(err).ServeHTTP(w, r)
|
||||
service.Authenticator.AuthenticationFailed(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
auth.OtherError(err).ServeHTTP(w, r)
|
||||
service.Authenticator.OtherError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := auth.UserFromSessionToken(cookie.Value)
|
||||
user, err := service.Authenticator.UserFromSessionToken(cookie.Value)
|
||||
if err == ErrNoUserForSession {
|
||||
auth.Logout(w)
|
||||
service.Logout(w)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
auth.OtherError(err).ServeHTTP(w, r)
|
||||
service.Authenticator.OtherError(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if config.RequireLogged {
|
||||
userPerms, err := auth.UserPermissions(userID)
|
||||
userPerms, err := service.Authenticator.UserPermissions((*user).UID())
|
||||
if err != nil {
|
||||
auth.OtherError(err).ServeHTTP(w, r)
|
||||
service.Authenticator.OtherError(err).ServeHTTP(w, r)
|
||||
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
|
||||
hasAll := true
|
||||
for _, perm := range config.WithPermissions {
|
||||
for _, perm := range config.NeedPermissions {
|
||||
if _, present := userPermsMap[perm]; !present {
|
||||
hasAll = false
|
||||
break
|
||||
@@ -144,14 +149,14 @@ func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Hand
|
||||
}
|
||||
|
||||
if !hasAll {
|
||||
auth.AuthenticationFailed(err).ServeHTTP(w, r)
|
||||
service.Authenticator.AuthenticationFailed(err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh session cookie expiration
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: SessionCookieName,
|
||||
Name: service.SessionCookieName,
|
||||
Path: "/",
|
||||
Value: cookie.Value,
|
||||
Expires: time.Now().Add(7 * 24 * time.Hour),
|
||||
@@ -166,30 +171,25 @@ func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Hand
|
||||
//
|
||||
// Middleware(*AuthMiddlewareConfig)
|
||||
//
|
||||
// that checks if a user is logged, no extra permissions are checked
|
||||
func (auth *AuthService) LoggedMiddleware() func(http.Handler) http.Handler {
|
||||
return auth.Middleware(&AuthMiddlewareConfig{
|
||||
// that only accepts logged in users, no special permissions are checked
|
||||
func (service *AuthSessionService[UserID, U]) LoggedMiddleware() func(http.Handler) http.Handler {
|
||||
return service.Middleware(&MiddlewareConfig{
|
||||
RequireLogged: true,
|
||||
WithPermissions: []string{},
|
||||
NeedPermissions: []string{},
|
||||
})
|
||||
}
|
||||
|
||||
// RequestUser retrives the "userID" from the given request based on the cookie session token.
|
||||
// When generics arrive this will become something like
|
||||
//
|
||||
// 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)
|
||||
// RequestUser retrieves the "userID" from the given request based on the cookie session token.
|
||||
func (service *AuthSessionService[UserID, U]) RequestUser(r *http.Request) (*U, error) {
|
||||
cookie, err := r.Cookie(service.SessionCookieName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userID, err := auth.UserFromSessionToken(cookie.Value)
|
||||
user, err := service.Authenticator.UserFromSessionToken(cookie.Value)
|
||||
if err == ErrNoUserForSession {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userID, nil
|
||||
return user, nil
|
||||
}
|
||||
|
||||
+12
-8
@@ -32,12 +32,16 @@ const (
|
||||
|
||||
// User represents a user in the database.
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Permissions util.StringSet `json:"permissions"`
|
||||
ID string `json:"id"`
|
||||
Permissions util.Set[string] `json:"permissions"`
|
||||
|
||||
// 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.
|
||||
type Room struct {
|
||||
ID string `json:"id"`
|
||||
@@ -96,8 +100,8 @@ type Seat struct {
|
||||
// Database Interfaces
|
||||
//
|
||||
|
||||
// Store is the main interface for interacting with database implementations, for now the only implementation is "memDB".
|
||||
type Store interface {
|
||||
// Database is the main interface for interacting with database implementations, for now the only implementation is "memDB".
|
||||
type Database interface {
|
||||
BeginTransaction()
|
||||
EndTransaction()
|
||||
|
||||
@@ -118,7 +122,7 @@ type Store interface {
|
||||
GetRoomOccupiedSeats(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
|
||||
FreeSeat(seatID string) error
|
||||
@@ -127,7 +131,7 @@ type Store interface {
|
||||
// TODO: Create an SQLite implementation
|
||||
|
||||
// NewInMemoryStore creates an instance of memDB hidden behind the Store interface.
|
||||
func NewInMemoryStore() Store {
|
||||
func NewInMemoryStore() Database {
|
||||
db := &memDB{
|
||||
&sync.Mutex{},
|
||||
|
||||
@@ -157,12 +161,12 @@ func NewInMemoryStore() Store {
|
||||
|
||||
db.users["aziis98"] = &User{
|
||||
ID: "aziis98",
|
||||
Permissions: util.NewStringSet(PermissionAdmin),
|
||||
Permissions: util.NewSet(PermissionAdmin),
|
||||
}
|
||||
|
||||
db.users["bachoseven"] = &User{
|
||||
ID: "bachoseven",
|
||||
Permissions: util.NewStringSet(PermissionAdmin),
|
||||
Permissions: util.NewSet(PermissionAdmin),
|
||||
}
|
||||
|
||||
return db
|
||||
|
||||
@@ -146,14 +146,14 @@ func (db *memDB) GetRoomFreeSeats(roomID string) ([]string, error) {
|
||||
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 {
|
||||
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 {
|
||||
|
||||
+33
-101
@@ -2,88 +2,35 @@ 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/httputil"
|
||||
"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"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
authService *auth.AuthService
|
||||
auth *auth.AuthSessionService[string, db.User]
|
||||
|
||||
roomServerEventStreams map[string]serverevents.Handler
|
||||
|
||||
Database db.Store
|
||||
Database db.Database
|
||||
ApiRoute chi.Router
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
sessions := make(map[string]*db.User)
|
||||
database := db.NewInMemoryStore()
|
||||
|
||||
server := &Server{
|
||||
Database: database,
|
||||
authService: &auth.AuthService{
|
||||
CheckUserPassword: func(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 := 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
|
||||
auth: auth.NewAuthSessionService[string, db.User](
|
||||
&simpleAuthenticator{
|
||||
make(map[string]string),
|
||||
database,
|
||||
},
|
||||
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),
|
||||
|
||||
@@ -103,7 +50,7 @@ func NewServer() *Server {
|
||||
func (server *Server) setupRoutes() {
|
||||
api := server.ApiRoute
|
||||
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
|
||||
// database.BeginTransaction()
|
||||
@@ -122,7 +69,7 @@ func (server *Server) setupRoutes() {
|
||||
return
|
||||
}
|
||||
|
||||
err := auth.Login(w, r, credentials.Username, credentials.Password)
|
||||
err := auth.Login(w, credentials.Username, credentials.Password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
@@ -138,17 +85,12 @@ func (server *Server) setupRoutes() {
|
||||
})
|
||||
|
||||
api.Get("/user", func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := auth.RequestUser(r)
|
||||
user, err := auth.RequestUser(r)
|
||||
if err != nil {
|
||||
httputil.WriteJSON(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := database.GetUser(userID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
}
|
||||
|
||||
httputil.WriteJSON(w, user)
|
||||
})
|
||||
|
||||
@@ -188,38 +130,40 @@ func (server *Server) setupRoutes() {
|
||||
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()).
|
||||
Post("/seat/occupy", func(w http.ResponseWriter, r *http.Request) {
|
||||
database.BeginTransaction()
|
||||
defer database.EndTransaction()
|
||||
|
||||
userID, err := auth.RequestUser(r)
|
||||
user, err := auth.RequestUser(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
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 {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
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
|
||||
if len(seat.OccupiedBy) == 0 {
|
||||
userSeats, err := database.GetUserSeats(userID)
|
||||
userSeats, err := database.GetUserSeats(user.ID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -230,7 +174,7 @@ func (server *Server) setupRoutes() {
|
||||
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)
|
||||
return
|
||||
}
|
||||
@@ -244,34 +188,22 @@ func (server *Server) setupRoutes() {
|
||||
|
||||
api.With(auth.LoggedMiddleware()).
|
||||
Post("/seat/leave", func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := auth.RequestUser(r)
|
||||
user, err := auth.RequestUser(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
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 {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 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 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)
|
||||
return
|
||||
}
|
||||
|
||||
+13
-15
@@ -4,12 +4,10 @@ import "encoding/json"
|
||||
|
||||
type present struct{}
|
||||
|
||||
// Waiting for Go 18...
|
||||
// type Set[T comparable] map[T]present
|
||||
type StringSet map[string]present
|
||||
type Set[T comparable] map[T]present
|
||||
|
||||
func NewStringSet(elements ...string) StringSet {
|
||||
set := StringSet{}
|
||||
func NewSet[T comparable](elements ...T) Set[T] {
|
||||
set := Set[T]{}
|
||||
|
||||
for _, elem := range elements {
|
||||
set.Add(elem)
|
||||
@@ -18,12 +16,12 @@ func NewStringSet(elements ...string) StringSet {
|
||||
return set
|
||||
}
|
||||
|
||||
func (set StringSet) Has(value string) bool {
|
||||
func (set Set[T]) Has(value T) bool {
|
||||
_, present := set[value]
|
||||
return present
|
||||
}
|
||||
|
||||
func (set StringSet) HasAny(values ...string) bool {
|
||||
func (set Set[T]) HasAny(values ...T) bool {
|
||||
for _, value := range values {
|
||||
if _, present := set[value]; present {
|
||||
return true
|
||||
@@ -32,7 +30,7 @@ func (set StringSet) HasAny(values ...string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (set StringSet) Contains(other StringSet) bool {
|
||||
func (set Set[T]) Contains(other Set[T]) bool {
|
||||
for v := range other {
|
||||
if !set.Has(v) {
|
||||
return false
|
||||
@@ -41,16 +39,16 @@ func (set StringSet) Contains(other StringSet) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (set StringSet) Add(value string) {
|
||||
func (set Set[T]) Add(value T) {
|
||||
set[value] = present{}
|
||||
}
|
||||
|
||||
func (set StringSet) Remove(value string) {
|
||||
func (set Set[T]) Remove(value T) {
|
||||
delete(set, value)
|
||||
}
|
||||
|
||||
func (set StringSet) Elements() []string {
|
||||
elements := []string{}
|
||||
func (set Set[T]) Elements() []T {
|
||||
elements := []T{}
|
||||
|
||||
for elem := range set {
|
||||
elements = append(elements, elem)
|
||||
@@ -59,12 +57,12 @@ func (set StringSet) Elements() []string {
|
||||
return elements
|
||||
}
|
||||
|
||||
func (set StringSet) MarshalJSON() ([]byte, error) {
|
||||
func (set Set[T]) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(set.Elements())
|
||||
}
|
||||
|
||||
func (set StringSet) UnmarshalJSON(data []byte) error {
|
||||
elements := []string{}
|
||||
func (set Set[T]) UnmarshalJSON(data []byte) error {
|
||||
elements := []T{}
|
||||
if err := json.Unmarshal(data, &elements); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user