Working in memory database for users and cookie authentication

main
Antonio De Lucreziis 3 years ago
parent 328271edc3
commit d8f1a9b4d2

@ -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)
}

@ -0,0 +1,12 @@
<!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>Login &bull; Lupus Lite</title>
</head>
<body>
<h1>Crea Partita | Lupus Lite</h1>
</body>
</html>

@ -8,7 +8,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IM+Fell+English:ital@0;1&family=Lato:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=IM+Fell+English:ital@0;1&family=Lato:ital,wght@0,300;0,400;0,900;1,300;1,400;1,900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/src/main.scss">
</head>
@ -16,9 +16,15 @@
<main>
<h1>Lupus Lite</h1>
<hr>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Labore qui quibusdam placeat officiis necessitatibus. Unde quis eos quo. Laborum, quis.</p>
<p>
Ottieni un link di accesso ad un partita oppure accedi e creane una nuova.
</p>
<hr>
<a href="/login" class="button">Login</a>
<div class="column">
<a href="/login" class="button">Registrati</a>
<a href="/login" class="button">Accedi</a>
<a href="/crea-partita" class="button">Crea Partita</a>
</div>
</main>
</body>
</html>

@ -4,9 +4,41 @@
<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>Login &bull; Lupus Lite</title>
<title>Lupus Lite</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IM+Fell+English:ital@0;1&family=Lato:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/src/main.scss">
</head>
<body>
<h1>Lupus Lite | Login</h1>
<main>
<h1>Lupus Lite</h1>
<form action="/api/login" method="post">
<h2 class="fill-row">Accedi</h2>
<p class="fill-row">
Inserisci le tue credenziali per accedere alla pagina utente
</p>
<label for="login-username">Username</label>
<input type="text" name="username" id="login-username">
<label for="login-password">Password</label>
<input type="password" name="password" id="login-password">
<button class="fill-row" type="submit">Accedi</button>
</form>
<form action="/api/register" method="post">
<h2 class="fill-row">Registrati</h2>
<p class="fill-row">
Crea un account per accedere alla pagina utente
</p>
<label for="register-username">Username</label>
<input type="text" name="username" id="register-username">
<label for="register-password">Password</label>
<input type="password" name="password" id="register-password">
<label for="register-password">Ripeti Password</label>
<input type="password" name="password2" id="register-password2">
<button class="fill-row" type="submit">Registrati</button>
</form>
</main>
</body>
</html>

@ -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);
}

@ -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')])
),
},
},

@ -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
)

@ -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=

@ -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())
})
}

@ -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)
}
Loading…
Cancel
Save