Login technically works

main
Antonio De Lucreziis 3 years ago
parent b5aecea322
commit 3aade31bd6

@ -10,9 +10,11 @@
</head> </head>
<body> <body>
<header> <header>
<div class="nav-cell left"></div> <div class="nav-cell left">
<div class="nav-item" id="clock">14:23</div>
</div>
<div class="nav-cell center"> <div class="nav-cell center">
<div class="nav-item">Prenota Posti DM</div> <div class="nav-item">Posti DM</div>
</div> </div>
<div class="nav-cell right"> <div class="nav-cell right">
<div class="nav-item">Login</div> <div class="nav-item">Login</div>
@ -21,52 +23,118 @@
<main> <main>
<div class="room-name">AulaStud</div> <div class="room-name">AulaStud</div>
<div class="room-main"> <div class="room-main">
<div class="room-diagram"> <div class="panel room-diagram">
<img src="./diargamma-posti.jpg" alt="diargamma-posti" /> <div
</div> class="posto libero"
<div class="room-bookings"> data-index="1"
style="grid-column: 3 / span 4; grid-row: 1 / span 2"
></div>
<div
class="posto libero"
data-index="2"
style="grid-column: 7 / span 4; grid-row: 1 / span 2"
></div>
<div
class="posto libero"
data-index="3"
style="grid-column: 1 / span 2; grid-row: 3 / span 4"
></div>
<div
class="posto libero"
data-index="4"
style="grid-column: 1 / span 2; grid-row: 7 / span 4"
></div>
<div
class="posto libero"
data-index="5"
style="grid-column: 3 / span 4; grid-row: 11 / span 2"
></div>
<div
class="posto occupato"
data-index="6"
style="grid-column: 7 / span 4; grid-row: 11 / span 2"
></div>
<div
class="posto occupato"
data-index="7"
style="grid-column: 8 / span 2; grid-row: 5 / span 4"
></div>
<div
class="posto occupato"
data-index="8"
style="grid-column: 14 / span 2; grid-row: 1 / span 4"
></div>
<div
class="posto occupato"
data-index="9"
style="grid-column: 14 / span 2; grid-row: 5 / span 4"
></div>
<div
class="posto occupato"
data-index="10"
style="grid-column: 14 / span 2; grid-row: 9 / span 4"
></div>
<div
class="posto occupato"
data-index="11"
style="grid-column: 14 / span 2; grid-row: 13 / span 4"
></div>
</div>
<div class="panel room-bookings">
<div class="seat-list"> <div class="seat-list">
<div class="seat"> <div class="seat">
<div class="name">Posto 1</div> <div class="name">Posto 1</div>
<button disabled>Prenotato</button> <button>Occupa</button>
</div> </div>
<div class="seat"> <div class="seat">
<div class="name">Posto 2</div> <div class="name">Posto 2</div>
<button disabled>Prenotato</button> <button>Occupa</button>
</div> </div>
<div class="seat"> <div class="seat">
<div class="name">Posto 3</div> <div class="name">Posto 3</div>
<button>Prenota</button> <button>Occupa</button>
</div> </div>
<div class="seat"> <div class="seat">
<div class="name">Posto 4</div> <div class="name">Posto 4</div>
<button>Prenota</button> <button>Occupa</button>
</div> </div>
<div class="seat"> <div class="seat selected">
<div class="name">Posto 5</div> <div class="name">Posto 5</div>
<button disabled>Prenotato</button> <button>Occupa</button>
</div> </div>
<div class="seat"> <div class="seat">
<div class="name">Posto 6</div> <div class="name">Posto 6</div>
<button disabled>Prenotato</button> <button disabled>Occupato</button>
</div> </div>
<div class="seat"> <div class="seat">
<div class="name">Posto 7</div> <div class="name">Posto 7</div>
<button>Prenota</button> <button disabled>Occupato</button>
</div> </div>
<div class="seat"> <div class="seat">
<div class="name">Posto 8</div> <div class="name">Posto 8</div>
<button>Prenota</button> <button disabled>Occupato</button>
</div> </div>
<div class="seat"> <div class="seat">
<div class="name">Posto 9</div> <div class="name">Posto 9</div>
<button disabled>Prenotato</button> <button disabled>Occupato</button>
</div> </div>
<div class="seat"> <div class="seat">
<div class="name">Posto 10</div> <div class="name">Posto 10</div>
<button>Prenota</button> <button disabled>Occupato</button>
</div>
</div> </div>
</div> </div>
<div class="panel room-timeline">
<button disabled>8:30 &mdash; 10:00</button>
<button disabled>10:00 &mdash; 11:00</button>
<button disabled>11:00 &mdash; 12:00</button>
<button disabled>12:00 &mdash; 13:00</button>
<button disabled>13:00 &mdash; 14:00</button>
<button>14:00 &mdash; 15:00</button>
<button>15:00 &mdash; 16:00</button>
<button>16:00 &mdash; 17:00</button>
<button disabled>17:00 &mdash; 18:00</button>
<button disabled>18:00 &mdash; 19:30</button>
</div> </div>
</div> </div>
</main> </main>

@ -55,6 +55,11 @@ header {
} }
} }
.panel {
border: 1px solid #ddd;
background: #fff;
}
main { main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -74,48 +79,169 @@ main {
} }
.room-main { .room-main {
display: flex; display: grid;
flex-direction: row;
gap: 1rem;
@media screen and (max-width: 1000px) { grid-template-columns: auto 1fr;
flex-direction: column; grid-template-rows: auto auto;
grid-template-areas:
'diagram bookings'
'timeline timeline';
max-width: 800px;
.room-diagram {
grid-area: diagram;
}
.room-bookings {
grid-area: bookings;
} }
.room-timeline {
grid-area: timeline;
}
gap: 1rem;
.room-diagram { .room-diagram {
padding: 1rem;
// --posto-libero-bg: #ddf;
// --posto-libero-bg-1: #88f;
// --posto-libero-bg-2: #66d;
--posto-libero-bg: #8d8;
--posto-occupato-bg: #d88;
display: grid;
grid-template-columns: repeat(15, 1fr);
grid-template-rows: repeat(16, 1fr);
gap: 0.25rem;
aspect-ratio: 15 / 16;
width: calc(15 * 2rem);
max-width: 500px;
.posto {
display: flex; display: flex;
align-items: center;
justify-content: center;
flex-direction: column; flex-direction: column;
border: 1px solid #ddd;
background: #fff; &.libero {
background: var(--posto-libero-bg);
}
&.occupato {
background: var(--posto-occupato-bg);
}
padding: 1rem; border: 2px solid #0004;
cursor: pointer;
&:hover {
border-color: #0006;
border-width: 4px;
}
line-height: 1;
&::before {
font-size: 12px;
content: 'Posto';
}
&::after {
font-size: 12px;
content: attr(data-index);
}
}
} }
.room-bookings { .room-bookings {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid #ddd;
background: #fff;
min-width: 15rem; min-width: 15rem;
width: 100%;
padding: 0.5rem; padding: 0.75rem 0;
.seat-list { .seat-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem;
.seat { .seat {
padding: 0.25rem 1rem;
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
height: 2.5rem;
&:hover {
background: #0001;
}
.name {
display: flex;
align-items: center;
}
&.selected {
background: #0002;
.name {
font-style: italic;
}
}
}
}
}
.room-timeline {
padding: 1rem;
display: flex;
flex-direction: row;
gap: 0.5rem;
align-items: center;
overflow-x: auto;
button {
min-width: 8rem;
height: 3rem;
} }
} }
} }
} }
@media screen and (max-width: 1000px) {
header {
height: unset;
display: flex;
flex-direction: column;
.nav-cell {
position: relative;
padding: 0 1rem;
}
}
main {
.room-main {
display: flex;
flex-direction: column;
.room-diagram {
width: 200px;
}
.room-timeline {
flex-direction: column;
}
}
}
} }
img { img {

@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:4000/api',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
},
},
})

@ -1,21 +1,29 @@
package auth package auth
import ( import (
"fmt" "errors"
"net/http" "net/http"
"time" "time"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/httputil"
) )
var (
ErrNoUserForSession = errors.New(`no user for session token`)
)
var SessionCookieName = "session" // TODO: Make configurable var SessionCookieName = "session" // TODO: Make configurable
type AuthMiddlewareConfig struct { type AuthMiddlewareConfig struct {
IsLogged bool // 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 WithPermissions []string
} }
type Authenticator interface { type Authenticator interface {
// Login adds a session cookie to the user // Login checks user credentials and adds a session cookie to the user if successfull
Login(w http.ResponseWriter, userID string) 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 clears the user session cookies (by setting the session cookie timeout to 0)
Logout(w http.ResponseWriter) Logout(w http.ResponseWriter)
@ -25,71 +33,106 @@ type Authenticator interface {
LoggedMiddleware() func(http.Handler) http.Handler LoggedMiddleware() func(http.Handler) http.Handler
// UserFromSession returns the userID for this cookie session token // UserFromSession returns the userID for this cookie session token
UserFromSession(r http.Request) (string, error) UserFromSession(r *http.Request) (string, error)
} }
type AuthService struct { type AuthService struct {
// CheckUserPassword // CheckUserPassword
CheckUserPassword func(userID string, password string) bool CheckUserPassword func(userID string, 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 UserPermissions func(userID string) ([]string, error)
// SessionTokenFromUser returns a session token that represents this user // SessionTokenFromUser returns a session token that represents this user
SessionTokenFromUser func(userID string) string SessionTokenFromUser func(userID string) (string, error)
// UserFromSessionToken returns the corresponing user for this session token // UserFromSessionToken returns the corresponing user for this session token or "auth.ErrNoUserForSession"
UserFromSessionToken func(session string) (string, bool) UserFromSessionToken func(session string) (string, error)
// AuthenticationFailed handles failed authentications // AuthenticationFailed handles failed authentications
AuthenticationFailed http.Handler AuthenticationFailed http.Handler
// OtherError handles other errors
OtherError func(error) http.Handler
}
func (auth *AuthService) Login(w http.ResponseWriter, r *http.Request, userID, password string) {
if err := auth.CheckUserPassword(userID, password); err != nil {
auth.AuthenticationFailed.ServeHTTP(w, r)
return
}
token, err := auth.SessionTokenFromUser(userID)
if err != nil {
auth.OtherError(err).ServeHTTP(w, r)
return
} }
func (auth *AuthService) Login(w http.ResponseWriter, userID string) {
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: SessionCookieName, Name: SessionCookieName,
Value: auth.SessionTokenFromUser(userID), Path: "/", // TODO: Make configurable
Expires: time.Now().Add(10 * time.Minute), // TODO: Make configurable Value: token,
Expires: time.Now().Add(7 * 24 * time.Hour), // TODO: Make configurable
}) })
httputil.WriteJSON(w, "ok")
} }
func (auth *AuthService) Logout(w http.ResponseWriter) { func (auth *AuthService) Logout(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: SessionCookieName, Name: SessionCookieName,
Path: "/",
Value: "", Value: "",
Expires: time.Now(), Expires: time.Now(),
}) })
httputil.WriteJSON(w, "ok")
} }
func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Handler) http.Handler { func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) 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(SessionCookieName)
if err == http.ErrNoCookie && !config.IsLogged { if err == http.ErrNoCookie {
if !config.RequireLogged { // Login not required
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
if err != nil {
auth.AuthenticationFailed.ServeHTTP(w, r) auth.AuthenticationFailed.ServeHTTP(w, r)
return return
} }
if err != nil {
auth.OtherError(err).ServeHTTP(w, r)
return
}
userID, exists := auth.UserFromSessionToken(cookie.Value) userID, err := auth.UserFromSessionToken(cookie.Value)
if !exists { if err == ErrNoUserForSession {
auth.Logout(w) auth.Logout(w)
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusUnauthorized)
return
}
if err != nil {
auth.OtherError(err).ServeHTTP(w, r)
return return
} }
if config.IsLogged { if config.RequireLogged {
userPerms := map[string]bool{} userPerms, err := auth.UserPermissions(userID)
for _, perm := range auth.UserPermissions(userID) { if err != nil {
userPerms[perm] = true auth.OtherError(err).ServeHTTP(w, r)
return
}
userPermsMap := map[string]bool{}
for _, perm := range userPerms {
userPermsMap[perm] = true
} }
// 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.WithPermissions {
if _, present := userPerms[perm]; !present { if _, present := userPermsMap[perm]; !present {
hasAll = false hasAll = false
break break
} }
@ -101,6 +144,14 @@ func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Hand
} }
} }
// Refresh session cookie expiration
http.SetCookie(w, &http.Cookie{
Name: SessionCookieName,
Path: "/",
Value: cookie.Value,
Expires: time.Now().Add(7 * 24 * time.Hour),
})
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
} }
@ -108,20 +159,20 @@ func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Hand
func (auth *AuthService) LoggedMiddleware() func(http.Handler) http.Handler { func (auth *AuthService) LoggedMiddleware() func(http.Handler) http.Handler {
return auth.Middleware(&AuthMiddlewareConfig{ return auth.Middleware(&AuthMiddlewareConfig{
IsLogged: true, RequireLogged: true,
WithPermissions: []string{}, WithPermissions: []string{},
}) })
} }
func (auth *AuthService) UserFromSession(r http.Request) (string, error) { func (auth *AuthService) UserFromSession(r *http.Request) (string, error) {
cookie, err := r.Cookie(SessionCookieName) cookie, err := r.Cookie(SessionCookieName)
if err != nil { if err != nil {
return "", err return "", err
} }
userID, exists := auth.UserFromSessionToken(cookie.Value) userID, err := auth.UserFromSessionToken(cookie.Value)
if !exists { if err == ErrNoUserForSession {
return "", fmt.Errorf(`no user for this session token`) return "", err
} }
return userID, nil return userID, nil

@ -1,50 +1,180 @@
package db package db
import "fmt"
// Entities // Entities
type Utente struct { type User struct {
ID string ID string
Username string Username string
Permissions []string Permissions []string
} }
type Stanza struct { type Room struct {
ID string ID string
Posti []string SeatIDs []string
// gridRows, gridCols int
} }
// Actions type Seat struct {
ID string
// OccupiedBy is an empty list or a singleton of a userID
OccupiedBy []string
type AzioneBase struct { // x, y, w, h int
Tipo string
} }
type AzioneStanza struct { // Database Interfaces
AzioneBase
UserID string type Store interface {
StanzaID string GetUser(userID string) (*User, error)
GetRoom(roomID string) (*Room, error)
GetSeat(seatID string) (*Seat, error)
GetOccupiedSeats(roomID string) ([]string, error)
GetFreeSeats(roomID string) ([]string, error)
GetUserSeat(userID string) ([]string, error)
OccupySeat(userID, roomID, seatID string) error
FreeSeat(userID, roomID, seatID string) error
} }
type AzioneOccupaPosto struct { // TODO: Create an SQLite implementation
AzioneStanza
PostoID string type memDB struct {
users map[string]*User
rooms map[string]*Room
seats map[string]*Seat
} }
type AzioneLiberaPosto struct { func NewInMemoryStore() Store {
AzioneStanza db := &memDB{
PostoID string make(map[string]*User),
make(map[string]*Room),
make(map[string]*Seat),
} }
// Database Interfaces db.rooms["aula-stud"] = &Room{
ID: "aula-stud",
SeatIDs: []string{
"aula-stud/posto-1",
"aula-stud/posto-2",
"aula-stud/posto-3",
"aula-stud/posto-4",
},
}
type Store interface { db.seats["aula-stud/posto-1"] = &Seat{
// Entities ID: "aula-stud/posto-1",
OccupiedBy: []string{},
}
db.seats["aula-stud/posto-2"] = &Seat{
ID: "aula-stud/posto-2",
OccupiedBy: []string{},
}
db.seats["aula-stud/posto-3"] = &Seat{
ID: "aula-stud/posto-3",
OccupiedBy: []string{},
}
db.seats["aula-stud/posto-4"] = &Seat{
ID: "aula-stud/posto-4",
OccupiedBy: []string{},
}
db.users["aziis98"] = &User{
ID: "aziis98",
Username: "aziis98",
Permissions: []string{"admin"},
}
db.users["bachoseven"] = &User{
ID: "bachoseven",
Username: "bachoseven",
Permissions: []string{"admin"},
}
return db
}
GetUtente(userID string) *Utente func (db *memDB) GetUser(userID string) (*User, error) {
GetStanza(stanzaID string) *Stanza user, present := db.users[userID]
if !present {
return nil, fmt.Errorf(`no such user "%s"`, userID)
}
// Available Seats return user, nil
}
func (db *memDB) GetRoom(roomID string) (*Room, error) {
room, present := db.rooms[roomID]
if !present {
return nil, fmt.Errorf(`no such room "%s"`, roomID)
}
return room, nil
}
func (db *memDB) GetSeat(seatID string) (*Seat, error) {
seat, present := db.seats[seatID]
if !present {
return nil, fmt.Errorf(`no such seat "%s"`, seatID)
}
return seat, nil
}
func (db *memDB) GetOccupiedSeats(roomID string) ([]string, error) {
room, err := db.GetRoom(roomID)
if err != nil {
return nil, err
}
seats := []string{}
for _, seatID := range room.SeatIDs {
seat := db.seats[seatID]
if len(seat.OccupiedBy) == 1 {
seats = append(seats, seatID)
}
}
return seats, nil
}
func (db *memDB) GetFreeSeats(roomID string) ([]string, error) {
room, err := db.GetRoom(roomID)
if err != nil {
return nil, err
}
seats := []string{}
for _, seatID := range room.SeatIDs {
seat := db.seats[seatID]
if len(seat.OccupiedBy) == 0 {
seats = append(seats, seatID)
}
}
return seats, nil
}
func (db *memDB) GetUserSeat(userID string) ([]string, error) {
for _, seat := range db.seats {
if len(seat.OccupiedBy) > 0 && seat.OccupiedBy[0] == userID {
return []string{userID}, nil
}
}
return []string{}, nil
}
func (db *memDB) OccupySeat(userID string, roomID string, seatID string) error {
db.seats[seatID].OccupiedBy = []string{userID}
return nil
}
GetPostiOccupati(stanzaID string) []string func (db *memDB) FreeSeat(userID string, roomID string, seatID string) error {
GetPostiLiberi(stanzaID string) []string db.seats[seatID].OccupiedBy = []string{}
return nil
} }

@ -4,4 +4,7 @@ go 1.17
require github.com/go-chi/chi/v5 v5.0.7 require github.com/go-chi/chi/v5 v5.0.7
require github.com/mattn/go-sqlite3 v1.14.12 // indirect require (
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae // indirect
github.com/mattn/go-sqlite3 v1.14.12 // indirect
)

@ -1,3 +1,5 @@
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae h1:zzGwJfFlFGD94CyyYwCJeSuD32Gj9GTaSi5y9hoVzdY=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=

@ -0,0 +1,17 @@
package httputil
import (
"encoding/json"
"net/http"
)
type H map[string]interface{}
func WriteJSON(w http.ResponseWriter, data interface{}) error {
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(data)
}
func ReadJSON(r *http.Request, data interface{}) error {
return json.NewDecoder(r.Body).Decode(data)
}

@ -2,37 +2,101 @@ package httputil
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
) )
func HandleSSE(handler func(broadcast, single chan<- string)) http.Handler { type SSEHandler struct {
broadcast := make(chan string) clients map[chan string]bool
Connected func(chan string)
Disconnected func(chan string)
}
func (sse *SSEHandler) init() {
if sse.clients == nil {
sse.clients = make(map[chan string]bool)
}
}
func (sse *SSEHandler) Broadcast(message string) {
sse.init()
for client := range sse.clients {
client <- message
}
}
func (sse *SSEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sse.init()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive") w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
client := make(chan string) client := make(chan string)
sse.clients[client] = true
log.Printf(`New connection`)
if sse.Connected != nil {
go sse.Connected(client)
}
defer func() { defer func() {
log.Printf(`Connection closed`)
close(client) close(client)
delete(sse.clients, client)
if sse.Disconnected != nil {
go sse.Disconnected(client)
}
}() }()
go handler(broadcast, client)
flusher, _ := w.(http.Flusher) flusher, _ := w.(http.Flusher)
for { for {
select { select {
case message := <-broadcast: case message, ok := <-client:
fmt.Fprintf(w, "%s\n", message) if !ok {
flusher.Flush() return
case message := <-client: }
fmt.Fprintf(w, "%s\n", message)
fmt.Fprintf(w, "data: %s\n\n", message)
flusher.Flush() flusher.Flush()
case <-r.Context().Done(): case <-r.Context().Done():
return return
} }
} }
})
} }
// func HandleSSE(handler func(broadcast, single chan<- string)) http.Handler {
// broadcast := make(chan string)
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// w.Header().Set("Content-Type", "text/event-stream")
// w.Header().Set("Cache-Control", "no-cache")
// w.Header().Set("Connection", "keep-alive")
// w.Header().Set("Access-Control-Allow-Origin", "*")
// client := make(chan string)
// defer func() {
// close(client)
// }()
// go handler(broadcast, client)
// flusher, _ := w.(http.Flusher)
// for {
// select {
// case message := <-broadcast:
// fmt.Fprintf(w, "data: %s\n\n", message)
// flusher.Flush()
// case message := <-client:
// fmt.Fprintf(w, "data: %s\n\n", message)
// flusher.Flush()
// case <-r.Context().Done():
// return
// }
// }
// })
// }

@ -1,6 +1,7 @@
package httputil_test package httputil_test
import ( import (
"log"
"net/http" "net/http"
"testing" "testing"
"time" "time"
@ -9,23 +10,23 @@ import (
) )
func TestSSE(t *testing.T) { func TestSSE(t *testing.T) {
var broadcastChannel chan<- string sse := &httputil.SSEHandler{
Connected: func(client chan string) {
log.Printf(`New client connected callback`)
client <- "Messaggio 1 per questo client"
time.Sleep(1 * time.Second)
client <- "Messaggio 2 per questo client"
},
}
go func() { go func() {
for { for {
if broadcastChannel != nil { log.Printf(`Broadcasting message`)
broadcastChannel <- "Messaggio per tutti" sse.Broadcast("Messaggio per tutti")
}
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
} }
}() }()
http.Handle("/sse", httputil.HandleSSE(func(broadcast, single chan<- string) { http.Handle("/sse", sse)
broadcastChannel = broadcast
time.Sleep(1 * time.Second)
single <- "Messaggio 1 per questo client"
single <- "Messaggio 2 per questo client"
}))
http.ListenAndServe(":8000", nil) http.ListenAndServe(":8000", nil)
} }

@ -8,7 +8,7 @@ import (
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
) )
var HOST = ":3000" var HOST = ":4000"
func main() { func main() {
r := chi.NewRouter() r := chi.NewRouter()
@ -18,6 +18,23 @@ func main() {
r.Use(middleware.Logger) r.Use(middleware.Logger)
r.Use(middleware.Recoverer) r.Use(middleware.Recoverer)
server := NewServer()
// api := chi.NewRouter()
// api.Get("/example", func(w http.ResponseWriter, r *http.Request) {
// w.Header().Add("Content-Type", "application/json")
// http.SetCookie(w, &http.Cookie{
// Name: "test",
// Value: "Prova",
// Expires: time.Now().Add(120 * time.Second),
// })
// json.NewEncoder(w).Encode(map[string]interface{}{
// "foo": "bar",
// })
// })
r.Mount("/api", server.ApiRoute)
log.Printf(`Starting server on %s...`, HOST) log.Printf(`Starting server on %s...`, HOST)
err := http.ListenAndServe(HOST, r) err := http.ListenAndServe(HOST, r)
if err != nil { if err != nil {

@ -0,0 +1,134 @@
package main
import (
"fmt"
"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/util"
"github.com/alecthomas/repr"
"github.com/go-chi/chi/v5"
)
type Server struct {
sessions map[string]*db.User
authService *auth.AuthService
Database db.Store
ApiRoute chi.Router
}
func NewServer() *Server {
server := &Server{
sessions: make(map[string]*db.User),
Database: db.NewInMemoryStore(),
ApiRoute: chi.NewRouter(),
}
server.authService = &auth.AuthService{
CheckUserPassword: func(userID, password string) error {
repr.Println("Sessions: ", server.sessions)
if password != "phc" {
return fmt.Errorf(`invalid password`)
}
return nil
},
UserPermissions: func(userID string) ([]string, error) {
user, err := server.Database.GetUser(userID)
if err != nil {
return nil, err
}
return user.Permissions, nil
},
SessionTokenFromUser: func(userID string) (string, error) {
user, err := server.Database.GetUser(userID)
if err != nil {
return "", err
}
token := util.RandomHash(10)
server.sessions[token] = user
return token, nil
},
UserFromSessionToken: func(session string) (string, error) {
user, present := server.sessions[session]
if !present {
return "", auth.ErrNoUserForSession
}
return user.ID, nil
},
AuthenticationFailed: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, `not authenticated`, 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)
})
},
}
server.setupRoutes()
return server
}
func (server *Server) setupRoutes() {
api := server.ApiRoute
db := server.Database
// Authenticated Routes
api.Post("/login", func(w http.ResponseWriter, r *http.Request) {
var credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := httputil.ReadJSON(r, &credentials); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
server.authService.Login(w, r, credentials.Username, credentials.Password)
})
api.With(server.authService.LoggedMiddleware()).
Post("/logout", func(w http.ResponseWriter, r *http.Request) {
server.authService.Logout(w)
})
api.With(server.authService.LoggedMiddleware()).
Get("/current-seat", func(w http.ResponseWriter, r *http.Request) {
userID, err := server.authService.UserFromSession(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
occupiedSeatID, err := db.GetUserSeat(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(occupiedSeatID) == 0 {
httputil.WriteJSON(w, nil)
return
}
seat, err := db.GetSeat(occupiedSeatID[0])
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
httputil.WriteJSON(w, seat)
})
}

@ -0,0 +1,13 @@
package util
import (
"crypto/rand"
"encoding/base64"
)
func RandomHash(len int) string {
buff := make([]byte, len)
rand.Read(buff)
str := base64.StdEncoding.EncodeToString(buff)
return str[:len]
}
Loading…
Cancel
Save