From d8f1a9b4d2f43996d12c2d752dbf14685fbf3ff2 Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Tue, 21 Jun 2022 01:20:02 +0200 Subject: [PATCH] Working in memory database for users and cookie authentication --- database.go | 136 +++++++++++++++++++++++++++++++++++++ frontend/crea-partita.html | 12 ++++ frontend/index.html | 12 +++- frontend/login.html | 36 +++++++++- frontend/src/main.scss | 105 ++++++++++++++++++++++++++-- frontend/vite.config.js | 9 ++- go.mod | 2 + go.sum | 4 ++ routes.go | 99 ++++++++++++++++++++++++++- utils.go | 22 ++++++ 10 files changed, 419 insertions(+), 18 deletions(-) create mode 100644 database.go create mode 100644 frontend/crea-partita.html create mode 100644 utils.go diff --git a/database.go b/database.go new file mode 100644 index 0000000..0e0aff6 --- /dev/null +++ b/database.go @@ -0,0 +1,136 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "golang.org/x/crypto/bcrypt" +) + +// PublicUser is the "public" version of the "User" struct, this excludes private information +type PublicUser struct { + Username string `json:"username"` +} + +// User represents a user in the database +type User struct { + Username string `json:"username"` + PasswordBCrypt []byte `json:"passwordBCrypt"` +} + +func (u User) PublicUser() PublicUser { + return PublicUser{ + Username: u.Username, + } +} + +type Database interface { + GetUsers() ([]User, error) + GetUser(username string) (User, error) + CreateUser(user User) error +} + +type memDB struct { + Users map[string]User `json:"users"` +} + +func NewInMemoryDB() (Database, error) { + var db memDB + + exampleFile, err := os.Open("example-db.local.json") + if err != nil { + return nil, err + } + + if err := json.NewDecoder(exampleFile).Decode(&db); err != nil { + return nil, err + } + + return &db, nil +} + +func (db *memDB) GetUsers() ([]User, error) { + users := make([]User, 0, len(db.Users)) + for _, u := range db.Users { + users = append(users, u) + } + + return users, nil +} + +func (db *memDB) GetUser(username string) (User, error) { + user, ok := db.Users[username] + if !ok { + return User{}, fmt.Errorf(`no user with username %q`, username) + } + + return user, nil +} + +func (db *memDB) CreateUser(user User) error { + if _, ok := db.Users[user.Username]; ok { + return fmt.Errorf(`user with username %q already exists`, user.Username) + } + + db.Users[user.Username] = user + return nil +} + +type Auth interface { + Register(username, password string) error + Login(username, password string) (string, error) + UserForSession(token string) (string, error) + GetUser(username string) (User, error) +} + +type memAuth struct { + db Database + + sessions map[string]string +} + +func NewInMemoryAuth(db Database) Auth { + return &memAuth{db, map[string]string{}} +} + +func (auth *memAuth) Register(username, password string) error { + passwordBCrypt, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return err + } + + return auth.db.CreateUser(User{ + Username: username, + PasswordBCrypt: passwordBCrypt, + }) +} + +func (auth *memAuth) Login(username, password string) (string, error) { + user, err := auth.db.GetUser(username) + if err != nil { + return "", err + } + + if err := bcrypt.CompareHashAndPassword(user.PasswordBCrypt, []byte(password)); err != nil { + return "", err + } + + token := GenerateRandomString(16) + auth.sessions[token] = username + + return token, nil +} + +func (auth *memAuth) UserForSession(token string) (string, error) { + username, ok := auth.sessions[token] + if !ok { + return "", fmt.Errorf(`invalid session token`) + } + + return username, nil +} + +func (auth *memAuth) GetUser(username string) (User, error) { + return auth.db.GetUser(username) +} diff --git a/frontend/crea-partita.html b/frontend/crea-partita.html new file mode 100644 index 0000000..d5b76f3 --- /dev/null +++ b/frontend/crea-partita.html @@ -0,0 +1,12 @@ + + + + + + + Login • Lupus Lite + + +

Crea Partita | Lupus Lite

+ + \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 122a604..5645ed9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,7 +8,7 @@ - + @@ -16,9 +16,15 @@

Lupus Lite


-

Lorem ipsum dolor sit amet consectetur adipisicing elit. Labore qui quibusdam placeat officiis necessitatibus. Unde quis eos quo. Laborum, quis.

+

+ Ottieni un link di accesso ad un partita oppure accedi e creane una nuova. +


- Login +
\ No newline at end of file diff --git a/frontend/login.html b/frontend/login.html index bf81b2d..e99d010 100644 --- a/frontend/login.html +++ b/frontend/login.html @@ -4,9 +4,41 @@ - Login • Lupus Lite + Lupus Lite + + + + + + -

Lupus Lite | Login

+
+

Lupus Lite

+
+

Accedi

+

+ Inserisci le tue credenziali per accedere alla pagina utente +

+ + + + + +
+
+

Registrati

+

+ Crea un account per accedere alla pagina utente +

+ + + + + + + +
+
\ No newline at end of file diff --git a/frontend/src/main.scss b/frontend/src/main.scss index 013cbfa..a180487 100644 --- a/frontend/src/main.scss +++ b/frontend/src/main.scss @@ -14,8 +14,10 @@ --ft-sans-wt-bold: 900; --accent-100: #f7dfdb; + --accent-350: #db6b59; --accent-400: #c55341; --accent-500: #a93523; + --accent-600: #7f2719; --bg-500: #222; } @@ -40,45 +42,90 @@ body { main { margin: 0 auto; - padding: 1rem 0.5rem; + padding: 1rem; max-width: 80ch; display: flex; flex-direction: column; align-items: center; + + gap: 2rem; + + @media screen and (max-width: 512px) { + align-items: stretch; + } } // // Components // +.column { + display: flex; + flex-direction: column; + align-items: stretch; + + gap: 1rem; +} + +.panel { + background: var(--accent-500); + box-shadow: 0 0 16px 0 #00000066; + + padding: 1rem; + border-radius: 2px; + + h1, + h2, + h3, + h4, + h5 { + color: var(--accent-100); + } +} + form { + @extend .panel; + display: grid; grid-template-columns: auto 1fr; - gap: 0.5rem 1rem; + gap: 1rem 0.5rem; max-width: 35rem; width: 100%; align-items: center; - .full-row { + .fill-row { grid-column: span 2; place-self: center; } label { + justify-self: end; font-weight: var(--ft-sans-wt-bold); } @media screen and (max-width: 512px) { grid-template-columns: 1fr; + gap: 0.25rem; - .full-row { + .fill-row { grid-column: span 1; place-self: stretch; } + + label { + justify-self: start; + align-self: end; + + padding-top: 0.5rem; + } + + button[type='submit'] { + margin-top: 1rem; + } } } @@ -91,11 +138,44 @@ a { button, .button { border: none; + border-radius: 2px; color: var(--accent-100); - background: var(--accent-500); + background: var(--accent-400); + + padding: 0.5rem 2rem; - padding: 0.5rem 1rem; + font-family: var(--ft-serif); + font-weight: var(--ft-serif-wt-regular); + font-size: 17px; + + display: flex; + align-items: center; + justify-content: center; + + cursor: pointer; + + &:hover { + background: var(--accent-350); + } +} + +input[type='text'], +input[type='password'], +textarea { + border: none; + border-radius: 2px; + + background: var(--accent-100); + color: var(--bg-500); + + font-family: var(--ft-sans); + font-size: 17px; + font-weight: var(--ft-sans-wt-regular); + + padding: 0.5rem; + + width: 100%; } // @@ -111,6 +191,7 @@ hr { p { margin: 0; + line-height: 1.75; } // Headings @@ -133,13 +214,23 @@ $heading-scale: 1.33; @for $i from 1 through 5 { h#{$i} { margin: 0; + text-align: center; font-family: var(--ft-serif); - color: var(--accent-400); $factor: pow($heading-scale, 5 - $i); font-size: $base-font-size * $factor; line-height: 1; + + text-shadow: 0 0 8px #00000066; } } + +h1 { + color: var(--accent-400); +} + +h2 { + color: var(--accent-350); +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 8e7bbdf..9e6012a 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,8 +1,7 @@ import { defineConfig } from 'vite' -import { basename, extname, resolve } from 'path' +import { resolve } from 'path' -const stripExt = path => basename(path, extname(path)) -const entryPoints = ['index.html', 'login.html', 'user.html', 'game.html'] +const entryPoints = ['index', 'login', 'user', 'game', 'crea-partita'] const redirect = redirectMap => ({ name: 'redirect', @@ -20,13 +19,13 @@ const redirect = redirectMap => ({ }, }) -const redirectMap = Object.fromEntries(entryPoints.map(path => ['/' + stripExt(path), '/' + path])) +const redirectMap = Object.fromEntries(entryPoints.map(page => ['/' + page, '/' + page + '.html'])) export default defineConfig({ build: { rollupOptions: { input: Object.fromEntries( - entryPoints.map(path => [stripExt(path), resolve(__dirname, path)]) + entryPoints.map(page => [page, resolve(__dirname, page + '.html')]) ), }, }, diff --git a/go.mod b/go.mod index efb3f09..807e1bc 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,13 @@ module github.com/aziis98/lupus-lite go 1.18 require ( + github.com/alecthomas/repr v0.1.0 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/gofiber/fiber/v2 v2.34.1 // indirect github.com/klauspost/compress v1.15.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.37.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect ) diff --git a/go.sum b/go.sum index cdddc76..0b4987c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= +github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/gofiber/fiber/v2 v2.34.1 h1:C6saXB7385HvtXX+XMzc5Dqj5S/aEXOfKCW7JNep4rA= @@ -11,6 +13,8 @@ github.com/valyala/fasthttp v1.37.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxn github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/routes.go b/routes.go index 7298a89..5f966e3 100644 --- a/routes.go +++ b/routes.go @@ -1,9 +1,106 @@ package main -import "github.com/gofiber/fiber/v2" +import ( + "encoding/json" + "log" + "time" + + "github.com/gofiber/fiber/v2" +) + +const UserKey = "github.com/aziis98/lupus-lite/user" + +func requestUser(c *fiber.Ctx) *User { + return c.Locals(UserKey).(*User) +} + +func RequireLoggedMiddleware(auth Auth) fiber.Handler { + return func(c *fiber.Ctx) error { + token := c.Cookies("sid") + username, err := auth.UserForSession(token) + if err != nil { + return err + } + + user, err := auth.GetUser(username) + if err != nil { + return err + } + + c.Locals(UserKey, &user) + + return c.Next() + } +} func mountApiRoutes(api fiber.Router) { + db, err := NewInMemoryDB() + if err != nil { + log.Fatal(err) + } + + auth := NewInMemoryAuth(db) + requireLogged := RequireLoggedMiddleware(auth) + api.Get("/status", func(c *fiber.Ctx) error { + s, err := json.MarshalIndent(db, "", " ") + if err != nil { + return err + } + + log.Println(string(s)) + return c.SendString("ok") }) + + api.Post("/login", func(c *fiber.Ctx) error { + var loginForm struct { + Username string `form:"username"` + Password string `form:"password"` + } + + if err := c.BodyParser(&loginForm); err != nil { + return err + } + + token, err := auth.Login(loginForm.Username, loginForm.Password) + if err != nil { + return err + } + + c.Cookie(&fiber.Cookie{ + Name: "sid", + Value: token, + Path: "/", + Expires: time.Now().Add(3 * 24 * time.Hour), + }) + + return c.Redirect("/") + }) + + api.Post("/logout", func(c *fiber.Ctx) error { + panic("TODO: not implemented") + }) + + api.Post("/register", func(c *fiber.Ctx) error { + var loginForm struct { + Username string `form:"username"` + Password string `form:"password"` + } + + if err := c.BodyParser(&loginForm); err != nil { + return err + } + + if err := auth.Register(loginForm.Username, loginForm.Password); err != nil { + return err + } + + return c.Redirect("/login") + }) + + api.Get("/user", requireLogged, func(c *fiber.Ctx) error { + return c.JSON(requestUser(c).PublicUser()) + }) + } diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..f73bd90 --- /dev/null +++ b/utils.go @@ -0,0 +1,22 @@ +package main + +import ( + "math/rand" + "time" +) + +const alphabet = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +func GenerateRandomString(n int) string { + b := make([]byte, n) + + for i := range b { + b[i] = alphabet[rand.Intn(len(alphabet))] + } + + return string(b) +}