diff --git a/.env.dev b/.env.dev index 46a5fbb..ee828f4 100644 --- a/.env.dev +++ b/.env.dev @@ -10,3 +10,6 @@ CHAT_URL=https://chat.phc.dm.unipi.it EMAIL=macchinisti@lists.dm.unipi.it USER_PAGES_BASE_URL=https://poisson.phc.dm.unipi.it/~ + +# AuthService +AUTH_SERVICE_HOST=:memory: diff --git a/.gitignore b/.gitignore index 08b1f19..c56dd4b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ node_modules/ # Don't version generated files public/js/*.min.js + +# Executables +phc-website-server diff --git a/README.md b/README.md index 8cbbad8..6c9e6c5 100644 --- a/README.md +++ b/README.md @@ -91,3 +91,9 @@ $ fd -e js | entr make js Rappresentano link ad altri servizi forniti, è comodo impostarli per testare tutto in locale su varie porte (e poi in produzione i vari url diventano link a sotto-domini del sito principale). Per ora ci sono `GIT_URL`, `CHAT_URL` e `USER_PAGES_BASE_URL`. + +## Altri Servizi + +Questo servizio dipende dal servizio di autenticazione per permettere agli utenti di autenticarsi usando vari meccanismi. + +Il servizio di autenticazione di default girerà su `localhost:3535` ed è documentato [sulla repo auth-service](https://git.phc.dm.unipi.it/phc/auth-service/) diff --git a/auth.go b/auth.go deleted file mode 100644 index 7d66df8..0000000 --- a/auth.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -// AuthService rappresenta un servizio di autenticazione -// type AuthService interface { -// GetUsers() []User - -// GetUser(username string) User - -// // LoginUser if successful returns the token for this user that will be stored in an HTTP cookie. -// LoginUser(username, password string) (string, error) -// } - -// LdapService ... -type LdapService struct { - URL string -} - -// FakeService ... -type FakeService struct { - URL string -} - -// NewAuthenticationService crea un nuovo servizio di autenticazione e controlla se è attivo -// func NewAuthenticationService(url string) (*LdapService, error) { -// service := new(LdapService) -// service.URL = url - -// res, err := service.Get("status") - -// if err != nil { -// return nil, err -// } - -// status, _ := ioutil.ReadAll(res.Body) - -// if string(status) != "true" { -// log.Fatalf("Authentication service isn't online, status: '%s'", status) -// } - -// return service, nil -// } diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..af1253c --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,42 @@ +package auth + +type User interface { + GetUsername() string + GetName() string + GetSurname() string + GetFullName() string +} + +type Session interface { + GetUsername() string + GetToken() string +} + +type AuthenticatorService interface { + GetUser(username string) (User, error) + GetUsers() ([]User, error) + GetSession(token string) (Session, error) + Login(username, password string) (Session, error) +} + +func UserForSession(as AuthenticatorService, token string) (User, error) { + session, err := as.GetSession(token) + if err != nil { + return nil, err + } + + user, err := as.GetUser(session.GetUsername()) + if err != nil { + return nil, err + } + + return user, nil +} + +func New(host string) AuthenticatorService { + if host == ":memory:" { + return exampleMemoryUsers + } + + return &LDAPAuthService{host} +} diff --git a/auth/ldap.go b/auth/ldap.go new file mode 100644 index 0000000..a8ebafb --- /dev/null +++ b/auth/ldap.go @@ -0,0 +1,138 @@ +package auth + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "path" + "time" +) + +type LDAPUser struct { + Username string `json:"username"` + NumericId int `json:"id"` + Name string `json:"name"` + Surname string `json:"surname"` + Email string `json:"email"` + Role string `json:"role"` + Gecos string `json:"gecos"` +} + +func (u LDAPUser) GetUsername() string { + return u.Username +} + +func (u LDAPUser) GetName() string { + return u.Name +} + +func (u LDAPUser) GetSurname() string { + return u.Surname +} + +func (u LDAPUser) GetFullName() string { + return u.Gecos +} + +type SimpleSession struct { + Token string `json:"token"` + Username string `json:"username"` + CreatedOn time.Time `json:"createdOn"` +} + +func (s SimpleSession) GetUsername() string { + return s.Username +} + +func (s SimpleSession) GetToken() string { + return s.Token +} + +type LDAPAuthService struct { + Host string +} + +func (a *LDAPAuthService) doGetRequest(url string, response interface{}) error { + req, err := http.NewRequest( + "GET", path.Join(a.Host, "ldap", url), bytes.NewBuffer([]byte("")), + ) + + if err != nil { + return err + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + return json.NewDecoder(res.Body).Decode(response) +} + +func (a *LDAPAuthService) doPostRequest(url string, request interface{}, response interface{}) error { + jsonStr, err := json.Marshal(request) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", path.Join(a.Host, "ldap", url), bytes.NewBuffer(jsonStr)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + return json.NewDecoder(res.Body).Decode(response) +} + +func (a *LDAPAuthService) GetUser(username string) (User, error) { + var user LDAPUser + if err := a.doGetRequest(fmt.Sprintf("/user/%s", username), &user); err != nil { + return nil, err + } + + return &user, nil +} + +func (a *LDAPAuthService) GetUsers() ([]User, error) { + ldapUsers := []*LDAPUser{} + if err := a.doGetRequest(fmt.Sprintf("/users"), &ldapUsers); err != nil { + return nil, err + } + + users := make([]User, len(ldapUsers)) + for i, u := range ldapUsers { + users[i] = u + } + + return users, nil +} + +func (a *LDAPAuthService) GetSession(token string) (Session, error) { + var response SimpleSession + if err := a.doGetRequest(fmt.Sprintf("/session/%s", token), &response); err != nil { + return nil, err + } + + return &response, nil +} + +func (a *LDAPAuthService) Login(username, password string) (Session, error) { + body := map[string]interface{}{ + "username": username, + "password": password, + } + + var response SimpleSession + if err := a.doPostRequest(fmt.Sprintf("/login"), body, &response); err != nil { + return nil, err + } + + return &response, nil +} diff --git a/auth/memory.go b/auth/memory.go new file mode 100644 index 0000000..efe9dde --- /dev/null +++ b/auth/memory.go @@ -0,0 +1,114 @@ +package auth + +import ( + "fmt" + + "git.phc.dm.unipi.it/phc/website/util" +) + +var exampleMemoryUsers = &Memory{ + Users: map[string]*MemoryUser{ + "aziis98": { + Username: "aziis98", + Name: "Antonio", + Surname: "De Lucreziis", + Password: "123", + }, + "bachoseven": { + Username: "bachoseven", + Name: "Francesco", + Surname: "Minnocci", + Password: "234", + }, + }, + Sessions: map[string]*MemorySession{}, +} + +type MemoryUser struct { + Username string `json:"username"` + Name string `json:"name"` + Surname string `json:"surname"` + + Password string `json:"-"` +} + +func (u *MemoryUser) GetUsername() string { + return u.Username +} + +func (u *MemoryUser) GetName() string { + return u.Name +} + +func (u *MemoryUser) GetSurname() string { + return u.Surname +} + +func (u *MemoryUser) GetFullName() string { + return u.Name + " " + u.Surname +} + +type MemorySession struct { + Username string + Token string +} + +func (s *MemorySession) GetUsername() string { + return s.Username +} + +func (s *MemorySession) GetToken() string { + return s.Token +} + +type Memory struct { + Users map[string]*MemoryUser + Sessions map[string]*MemorySession +} + +func (m *Memory) GetUser(username string) (User, error) { + user, ok := m.Users[username] + if !ok { + return nil, fmt.Errorf(`no user with that username`) + } + + return user, nil +} + +func (m *Memory) GetUsers() ([]User, error) { + users := make([]User, len(m.Users)) + i := 0 + for _, u := range m.Users { + users[i] = u + i++ + } + + return users, nil +} + +func (m *Memory) GetSession(token string) (Session, error) { + session, ok := m.Sessions[token] + if !ok { + return nil, fmt.Errorf(`invalid session token`) + } + + return session, nil +} + +func (m *Memory) Login(username string, password string) (Session, error) { + user, err := m.GetUser(username) + if err != nil { + return nil, err + } + + memUser := user.(*MemoryUser) + + if memUser.Password != password { + return nil, fmt.Errorf(`invalid credentials`) + } + + session := &MemorySession{username, util.GenerateRandomString(15)} + m.Sessions[session.Token] = session + + return session, nil +} diff --git a/config/config.go b/config/config.go index 4dd4ec5..11ae1ab 100644 --- a/config/config.go +++ b/config/config.go @@ -18,6 +18,8 @@ var Email string var UserPagesBaseUrl string +var AuthServiceHost string + func loadEnv(target *string, name, defaultValue string) { value := os.Getenv(name) if len(strings.TrimSpace(value)) == 0 { @@ -30,17 +32,22 @@ func loadEnv(target *string, name, defaultValue string) { func Load() { godotenv.Load() - log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + // Production loadEnv(&Mode, "MODE", "production") loadEnv(&Host, "HOST", "localhost:8080") + // Services loadEnv(&GitUrl, "GIT_URL", "https://git.example.org") loadEnv(&ChatUrl, "CHAT_URL", "https://chat.example.org") loadEnv(&Email, "EMAIL", "mail@example.org") + // Poisson loadEnv(&UserPagesBaseUrl, "USER_PAGES_BASE_URL", "https://poisson.phc.dm.unipi.it/~") + + // AuthService + loadEnv(&AuthServiceHost, "AUTH_SERVICE_HOST", "http://localhost:3535") } func Object() util.H { @@ -53,5 +60,7 @@ func Object() util.H { "Email": Email, "UserPagesBaseUrl": UserPagesBaseUrl, + + "AuthServiceHost": AuthServiceHost, } } diff --git a/main.go b/main.go index 3e98ba4..4da8ab4 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,12 @@ package main import ( + "fmt" "html/template" + "time" "git.phc.dm.unipi.it/phc/website/articles" + "git.phc.dm.unipi.it/phc/website/auth" "git.phc.dm.unipi.it/phc/website/config" "git.phc.dm.unipi.it/phc/website/templates" "git.phc.dm.unipi.it/phc/website/util" @@ -13,6 +16,15 @@ import ( "github.com/gofiber/redirect/v2" ) +func UserMiddleware(as auth.AuthenticatorService) fiber.Handler { + return func(c *fiber.Ctx) error { + token := c.Cookies("session-token") + user, _ := auth.UserForSession(as, token) + c.Locals("user", user) + return c.Next() + } +} + func main() { config.Load() @@ -29,6 +41,9 @@ func main() { // Static content app.Static("/public/", "./public") + authService := auth.New(config.AuthServiceHost) + app.Use(UserMiddleware(authService)) + // Templates & Renderer renderer := templates.NewRenderer( "./views/", @@ -51,7 +66,9 @@ func main() { localView := view app.Get(route, func(c *fiber.Ctx) error { c.Type("html") - return renderer.Render(c, localView, util.H{}) + return renderer.Render(c, localView, util.H{ + "User": c.Locals("user"), + }) }) } @@ -72,6 +89,7 @@ func main() { c.Type("html") return renderer.Render(c, "storia.html", util.H{ + "User": c.Locals("user"), "Storia": storia, }) }) @@ -81,6 +99,7 @@ func main() { c.Type("html") return renderer.Render(c, "appunti.html", util.H{ + "User": c.Locals("user"), "Query": searchQuery, }) }) @@ -93,10 +112,50 @@ func main() { c.Type("html") return renderer.Render(c, "news.html", util.H{ + "User": c.Locals("user"), "Articles": articles, }) }) + app.Post("/login", func(c *fiber.Ctx) error { + var loginForm struct { + Provider string `form:"provider"` + Username string `form:"username"` + Password string `form:"password"` + } + + if err := c.BodyParser(&loginForm); err != nil { + return err + } + + session, err := authService.Login(loginForm.Username, loginForm.Password) + if err != nil { + return err + } + + inThreeDays := time.Now().Add(3 * 24 * time.Hour) + c.Cookie(&fiber.Cookie{ + Name: "session-token", + Path: "/", + Value: session.GetToken(), + Expires: inThreeDays, + }) + + return c.Redirect("/profilo") + }) + + app.Get("/profilo", func(c *fiber.Ctx) error { + user, ok := c.Locals("user").(auth.User) + if !ok || user == nil { + return fmt.Errorf(`no logged in user`) + } + + c.Type("html") + return renderer.Render(c, "profilo.html", util.H{ + "User": c.Locals("user"), + }) + }) + app.Get("/news/:article", func(c *fiber.Ctx) error { articleID := c.Params("article") @@ -112,6 +171,7 @@ func main() { c.Type("html") return renderer.Render(c, "news-base.html", util.H{ + "User": c.Locals("user"), "Article": article, "ContentHTML": template.HTML(html), }) diff --git a/public/style.css b/public/style.css index 555b98f..bb68780 100644 --- a/public/style.css +++ b/public/style.css @@ -322,13 +322,15 @@ section { .card .title { font-size: 22px; font-weight: var(--font-weight-bold); + + padding-bottom: 0.5rem; } .card .date { font-size: 15px; color: var(--card-date); - margin-bottom: 0.75rem; + padding-bottom: 0.5rem; } .card .description { @@ -341,7 +343,7 @@ section { flex-direction: row; gap: 0 0.5rem; - margin-top: 0.5rem; + padding-top: 0.5rem; } .tags .tag { @@ -393,9 +395,10 @@ p, ul, ol, li { - margin: 0.5rem 0; + margin: 0; width: 70ch; max-width: 100%; + line-height: 1.8; } ul, @@ -832,6 +835,10 @@ form .field-set input { /* Rendered Markdown */ +.news-content p { + margin: 0.5rem 0; +} + .news-content { display: block; } diff --git a/util/generics.go b/util/generics.go new file mode 100644 index 0000000..c7d8682 --- /dev/null +++ b/util/generics.go @@ -0,0 +1 @@ +package util diff --git a/util/rand.go b/util/rand.go new file mode 100644 index 0000000..aa82370 --- /dev/null +++ b/util/rand.go @@ -0,0 +1,18 @@ +package util + +import ( + "crypto/rand" + "encoding/base64" + "log" +) + +func GenerateRandomString(n int) string { + b := make([]byte, n) + + _, err := rand.Read(b) + if err != nil { + log.Fatal(err) + } + + return base64.URLEncoding.EncodeToString(b) +} diff --git a/views/appunti.html b/views/appunti.html index 0fb0ad4..7100cd9 100644 --- a/views/appunti.html +++ b/views/appunti.html @@ -4,10 +4,10 @@ {{define "body"}}
-

+

Raccolta degli Appunti -

+

Questa è la raccolta degli appunti presenti su Poisson. Cerca il titolo della dispensa, il nome e cognome o l'username dell'autore oppure scrivi il nome del corso rispetto a cui filtrare. Altrimenti in cima compariranno gli appunti più "gettonati".

diff --git a/views/home.html b/views/home.html index 001271e..fa2b6a0 100644 --- a/views/home.html +++ b/views/home.html @@ -28,8 +28,9 @@
yyyy-mm-dd
- much doge, ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur... - +

+ much doge, ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur... +

@@ -38,8 +39,9 @@
yyyy-mm-dd
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur... - +

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur... +

@@ -48,8 +50,9 @@
yyyy-mm-dd
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur... - +

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis nemo aperiam, voluptas quam alias esse sed natus tempore suscipit fugiat sit delectus exercitationem numquam ipsum assumenda recusandae consequatur... +

diff --git a/views/link.html b/views/link.html index 8699b4c..5f2f049 100644 --- a/views/link.html +++ b/views/link.html @@ -4,10 +4,10 @@ {{define "body"}}
-

+

Link Utili -

+

Questo è un elenco di alcuni indirizzi potenzialmente utili

@@ -19,7 +19,9 @@
- Lista di link relativi alle attività dell'aula studenti del Dipartimento di Matematica +

+ Lista di link relativi alle attività dell'aula studenti del Dipartimento di Matematica +

@@ -28,7 +30,9 @@
- Sito per chiedere il recupero/reset delle proprie credenziali Poisson; le matricole possono inserire direttamente le loro credenziali di Alice ed ottenere quelle del loro account su Poisson. +

+ Sito per chiedere il recupero/reset delle proprie credenziali Poisson; le matricole possono inserire direttamente le loro credenziali di Alice ed ottenere quelle del loro account su Poisson. +

@@ -37,7 +41,9 @@
- Questo sito ti permette di disegnare "a mano" il simbolo che cerchi e trovare il suo corrispondente comando in LaTex. +

+ Questo sito ti permette di disegnare "a mano" il simbolo che cerchi e trovare il suo corrispondente comando in LaTex. +

@@ -46,7 +52,9 @@
- Un semplice shell script che permette di stampare un file in una stampante del Dipartimento di Matematica da una qualsiasi shell Unix (Linux, MacOS, BSD...). Funziona anche da remoto! +

+ Un semplice shell script che permette di stampare un file in una stampante del Dipartimento di Matematica da una qualsiasi shell Unix (Linux, MacOS, BSD...). Funziona anche da remoto! +

@@ -55,7 +63,9 @@
- Sei stanco del pessimo font di Wikipedia? Finalmente potrai leggere i tuoi articoli preferiti di Wikipedia con un typesetting in stile LaTex! +

+ Sei stanco del pessimo font di Wikipedia? Finalmente potrai leggere i tuoi articoli preferiti di Wikipedia con un typesetting in stile LaTex! +

@@ -64,7 +74,9 @@
- La homepage del corso di studi all'interno del sito ufficiale del Dipartimento di Matematica +

+ La homepage del corso di studi all'interno del sito ufficiale del Dipartimento di Matematica +

diff --git a/views/login.html b/views/login.html index 506e9af..3096b39 100644 --- a/views/login.html +++ b/views/login.html @@ -4,10 +4,10 @@ {{define "body"}}
-

+

Account di Poisson -

+
@@ -15,10 +15,13 @@ Accedi

- Inserisci le tue credenziali di Poisson per accedere. + Inserisci le tue credenziali di Poisson per accedere

+ + + @@ -35,11 +38,10 @@

Ottenere un account

- Se vuoi ottenere un account compila* il modulo di richiesta e portacelo in PHC o inviacelo via - email all'indirizzo {{ .Config.Email }}. + Dal 2022 in nuovi utenti hanno bisogno di compilare un modulo se vogliono ottenere un account1, scarica il modulo di richiesta e portacelo in PHC o inviacelo via email all'indirizzo {{ .Config.Email }}.

- *In realtà il modulo ancora non esiste + 1: In realtà il modulo ancora non esiste

@@ -47,7 +49,7 @@

Recupero credenziali

Per il recupero credenziali vieni direttamente al PHC a parlarne con calma con noi altrimenti puoi inviaci una - email all'indirizzo {{ .Config.Email }}. + email all'indirizzo {{ .Config.Email }} e poi recuperare le nuove credenziali sul sito credenziali.phc.dm.unipi.it.

{{end}} \ No newline at end of file diff --git a/views/news.html b/views/news.html index f832153..6932331 100644 --- a/views/news.html +++ b/views/news.html @@ -4,10 +4,10 @@ {{define "body"}}
-

+

Notizie Importanti -

+
{{ range .Articles }} {{ if .HasTag "important" }} @@ -18,7 +18,9 @@
{{ .PublishDate.Format "2006/01/02" }}
-
{{ .Description }}
+
+

{{ .Description }}

+
{{ range .Tags }} {{ . }} @@ -45,7 +47,9 @@
{{ .PublishDate.Format "2006/01/02" }}
-
{{ .Description }}
+
+

{{ .Description }}

+
{{ range .Tags }} {{ . }} diff --git a/views/partials/navbar.html b/views/partials/navbar.html index d1c7490..42020c4 100644 --- a/views/partials/navbar.html +++ b/views/partials/navbar.html @@ -76,7 +76,7 @@
{{if .User}} {{else}}