Working in memory database for users and cookie authentication
This commit is contained in:
+136
@@ -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 • Lupus Lite</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Crea Partita | Lupus Lite</h1>
|
||||
</body>
|
||||
</html>
|
||||
+9
-3
@@ -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>
|
||||
+34
-2
@@ -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 • 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>
|
||||
+98
-7
@@ -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 1rem;
|
||||
padding: 0.5rem 2rem;
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user