Initial commit

This commit is contained in:
2026-06-03 01:52:56 +02:00
commit 190134af7b
17 changed files with 772 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
appdata
.idea
*.exe
*.cmd
.run

42
auth/jwt.go Normal file
View File

@@ -0,0 +1,42 @@
package auth
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
UserID string `json:"user_id"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func GenerateJWT(userID, role string, secret []byte) (string, error) {
claims := Claims{
UserID: userID,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secret)
}
func ValidateJWT(tokenStr string, secret []byte) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return secret, nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, err
}
return claims, nil
}

48
auth/middleware.go Normal file
View File

@@ -0,0 +1,48 @@
package auth
import (
"context"
"net/http"
"strings"
)
type contextKey string
const UserContextKey contextKey = contextKey("user")
func AuthMiddleware(secret []byte) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing token", http.StatusUnauthorized)
return
}
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := ValidateJWT(tokenStr, secret)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), UserContextKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func RequireRole(role string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value(UserContextKey).(*Claims)
if claims.Role != role {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}

12
auth/password.go Normal file
View File

@@ -0,0 +1,12 @@
package auth
import "golang.org/x/crypto/bcrypt"
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

60
config/config.go Normal file
View File

@@ -0,0 +1,60 @@
package config
import (
"log"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
type Config struct {
Port string `yaml:"port"`
DatabasePath string `yaml:"database_path"`
CertificatePath string `yaml:"certificate_path"`
PrivateKeyPath string `yaml:"private_key_path"`
}
const configPath = "./appdata/config.yaml"
func CheckIfExists() error {
_, err := os.Stat(configPath)
if err == nil {
return nil
}
if !os.IsNotExist(err) {
return err
}
log.Printf("Config file %s doesn't exist, creating...", configPath)
err = os.MkdirAll(filepath.Dir(configPath), 0755)
if err != nil {
return err
}
defaultConfig := Config{
Port: "8080",
DatabasePath: "./appdata/database.db",
CertificatePath: "./appdata/cert.pem",
PrivateKeyPath: "./appdata/key.pem",
}
data, err := yaml.Marshal(defaultConfig)
if err != nil {
return err
}
return os.WriteFile(configPath, data, 0644)
}
func LoadConfig() (Config, error) {
var cfg Config
file, err := os.ReadFile(configPath)
if err != nil {
return cfg, err
}
err = yaml.Unmarshal(file, &cfg)
return cfg, err
}

19
frontend/handler.go Normal file
View File

@@ -0,0 +1,19 @@
package frontend
import (
"html/template"
"net/http"
)
func Home(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
var tmpl = template.Must(template.ParseFiles("frontend/htmx/home.html"))
err := tmpl.Execute(w, struct {
Name string
}{
Name: "Miau",
})
if err != nil {
return
}
}

6
frontend/htmx/home.html Normal file
View File

@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<h1>Hallo, {{.Name}}!</h1>
</body>
</html>

22
go.mod Normal file
View File

@@ -0,0 +1,22 @@
module MiauInv
go 1.26
require (
github.com/glebarez/go-sqlite v1.22.0
github.com/golang-jwt/jwt/v5 v5.3.1
golang.org/x/crypto v0.52.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.45.0 // indirect
modernc.org/libc v1.37.6 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.28.0 // indirect
)

31
go.sum Normal file
View File

@@ -0,0 +1,31 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=

244
handlers/account.go Normal file
View File

@@ -0,0 +1,244 @@
package handlers
import (
"MiauInv/auth"
"MiauInv/config"
"MiauInv/models"
"MiauInv/storage"
"MiauInv/util"
"encoding/json"
"log"
"net/http"
)
var cfg, _ = config.LoadConfig()
func Register(w http.ResponseWriter, r *http.Request) {
var user models.User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
log.Println("POST [api/register] " + r.RemoteAddr + ": " + err.Error())
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if user.Username == "" || user.Password == "" {
log.Println("POST [api/register] " + r.RemoteAddr + ": Username or Password is empty")
http.Error(w, "username and password required", http.StatusBadRequest)
return
}
hashed, err := auth.HashPassword(user.Password)
if err != nil {
log.Println("POST [api/register] " + r.RemoteAddr + ": " + err.Error())
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
user.Password = hashed
user.ID = utils.GenerateUUID()
user.Role = models.RoleUser
//if err := storage.AddUser(&user); err != nil {
// log.Println("POST [api/register] " + r.RemoteAddr + ": " + err.Error())
// http.Error(w, "user already exists", http.StatusBadRequest)
// return
//}
w.WriteHeader(http.StatusCreated)
log.Println("POST [api/register] " + r.RemoteAddr + ": Successfully created user")
}
func Login(w http.ResponseWriter, r *http.Request) {
//var creds struct {
// Username string `json:"username"`
// Password string `json:"password"`
//}
//if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
// log.Println("POST [api/login] " + r.RemoteAddr + ": " + err.Error())
// http.Error(w, "Invalid request", http.StatusBadRequest)
// return
//}
//
//user, err := storage.GetUserByUsername(creds.Username)
//if err != nil {
// log.Println("POST [api/login] " + r.RemoteAddr + ": " + err.Error())
// http.Error(w, "Invalid credentials", http.StatusUnauthorized)
// return
//}
//
//if !auth.CheckPasswordHash(creds.Password, user.Password) {
// log.Println("POST [api/login] " + r.RemoteAddr + ": Invalid credentials")
// http.Error(w, "Invalid credentials", http.StatusUnauthorized)
// return
//}
//
//secret := []byte(os.Getenv("SHAP_JWT_SECRET"))
//if len(secret) == 0 {
// log.Println("POST [api/login] " + r.RemoteAddr + ": Server misconfiguration")
// http.Error(w, "Server misconfiguration", http.StatusInternalServerError)
// return
//}
//
//accessToken, err := auth.GenerateJWT(user.ID, user.Role, secret)
//if err != nil {
// log.Println("POST [api/login] " + r.RemoteAddr + ": " + err.Error())
// http.Error(w, "Could not generate token", http.StatusInternalServerError)
// return
//}
//
//refreshTokenPlain, err := utils.GenerateRefreshToken()
//if err != nil {
// log.Println("POST [api/login] " + r.RemoteAddr + ": " + err.Error())
// http.Error(w, "could not generate refresh token", http.StatusInternalServerError)
// return
//}
//refreshHash := utils.HashToken(refreshTokenPlain)
//refreshID := utils.GenerateUUID()
//refreshExpires := time.Now().Add(7 * 24 * time.Hour).Unix() // expiry: 7 days
//
//deviceInfo := r.Header.Get("User-Agent")
//
//if err := storage.AddRefreshToken(&models.RefreshToken{
// ID: refreshID,
// UserID: user.ID,
// Token: refreshHash,
// ExpiresAt: refreshExpires,
// DeviceInfo: deviceInfo,
// CreatedAt: time.Now().Unix(),
// Revoked: false,
//}); err != nil {
// log.Println("POST [api/login] " + r.RemoteAddr + ": " + err.Error())
// http.Error(w, "could not save refresh token", http.StatusInternalServerError)
// return
//}
//
//// Return access + refresh token (refresh in plain for client to store securely)
//resp := map[string]interface{}{
// "access_token": accessToken,
// "refresh_token": refreshTokenPlain,
// "user": map[string]interface{}{
// "id": user.ID,
// "username": user.Username,
// "role": user.Role,
// },
// "wgName": cfg.HouseholdName,
//}
//
//w.Header().Set("Content-Type", "application/json")
//err = json.NewEncoder(w).Encode(resp)
//if err != nil {
// log.Println("POST [api/login] " + r.RemoteAddr + ": " + err.Error())
// http.Error(w, "Something went wrong", http.StatusInternalServerError)
// return
//}
//log.Println("POST [api/login] " + r.RemoteAddr + ": Successfully logged in")
}
func Logout(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value(auth.UserContextKey).(*auth.Claims)
err := storage.RevokeAllRefreshTokensForUser(claims.UserID)
if err != nil {
log.Println("GET [api/logout] " + r.RemoteAddr + ": " + err.Error())
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.WriteHeader(204)
}
func TestHandler(w http.ResponseWriter, r *http.Request) {
claims, _ := utils.IsLoggedIn(w, r)
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": claims.UserID,
"role": claims.Role,
"msg": "Authentication successful",
})
if err != nil {
log.Println("GET [api/ping] " + r.RemoteAddr + ": " + err.Error())
return
}
log.Println("GET [api/login] " + r.RemoteAddr + ": Successfully tested connection")
}
func RefreshToken(w http.ResponseWriter, r *http.Request) {
//var req struct {
// RefreshToken string `json:"refresh_token"`
//}
//if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
// log.Println("POST [api/refresh] " + r.RemoteAddr + ": " + err.Error())
// http.Error(w, "Invalid request", http.StatusBadRequest)
// return
//}
//
//hashed := utils.HashToken(req.RefreshToken)
//
//tokenRow, err := storage.GetRefreshToken(hashed)
//if err != nil || tokenRow.Revoked || tokenRow.ExpiresAt < time.Now().Unix() {
// log.Println("POST [api/refresh] " + r.RemoteAddr + ": Invalid refresh token")
// http.Error(w, "Invalid refresh token", http.StatusUnauthorized)
// return
//}
//
//if err := storage.RevokeRefreshToken(tokenRow.ID); err != nil {
// log.Println(err)
//}
//
//newToken, _ := utils.GenerateRefreshToken()
//newHash := utils.HashToken(newToken)
//newExpires := time.Now().Add(7 * 24 * time.Hour).Unix() //7 days
//newID := utils.GenerateUUID()
//deviceInfo := r.Header.Get("User-Agent")
//if err = storage.AddRefreshToken(&models.RefreshToken{
// ID: newID,
// UserID: tokenRow.UserID,
// Token: newHash,
// ExpiresAt: newExpires,
// CreatedAt: time.Now().Unix(),
// Revoked: false,
// DeviceInfo: deviceInfo,
//}); err != nil {
// log.Println("POST [api/refresh] " + r.RemoteAddr + ": " + err.Error())
// http.Error(w, "Could not generate new refresh token", http.StatusInternalServerError)
// return
//}
//
//user, err := storage.GetUserById(tokenRow.UserID)
//if err != nil {
// log.Println("POST [api/refresh] " + r.RemoteAddr + ": " + err.Error())
// http.Error(w, "Internal server error", http.StatusInternalServerError)
// return
//}
//accessToken, _ := auth.GenerateJWT(tokenRow.UserID, user.Role, []byte(os.Getenv("SHAP_JWT_SECRET")))
//
//if err = json.NewEncoder(w).Encode(map[string]string{
// "access_token": accessToken,
// "refresh_token": newToken,
//}); err != nil {
// log.Println("POST [api/refresh] " + r.RemoteAddr + ": " + err.Error())
// http.Error(w, "Internal server error", http.StatusInternalServerError)
// return
//}
//log.Println("POST [api/refresh] " + r.RemoteAddr + ": Successfully refreshed token")
}
func UserInfo(w http.ResponseWriter, r *http.Request) {
//if r.Method != http.MethodGet {
// log.Println("GET [api/userinfo] " + r.RemoteAddr + ": Method " + r.Method + " not allowed")
// http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
// return
//}
//query := r.URL.Query()
//idParam := query.Get("id")
//user, err := storage.GetUserById(idParam)
//if err != nil {
// log.Println("GET [api/userinfo] " + r.RemoteAddr + ": User " + idParam + " not found")
// http.Error(w, "User not found", http.StatusNotFound)
// return
//}
//w.Header().Set("Content-Type", "application/json")
//err = json.NewEncoder(w).Encode(map[string]interface{}{
// "id": user.ID,
// "name": user.Username,
// "avatar_url": "",
//})
//if err != nil {
// log.Println("GET [api/userinfo] " + r.RemoteAddr + ": " + err.Error())
// return
//}
//log.Println("GET [api/userinfo] " + r.RemoteAddr + ": Successfully retrieved user info")
}

19
main.go Normal file
View File

@@ -0,0 +1,19 @@
package main
import (
"MiauInv/server"
"MiauInv/storage"
"log"
)
func main() {
var _server = server.InitServer()
err := storage.InitDB(_server.DatabasePath)
if err != nil {
log.Fatal(err)
return
}
_server.Run()
}

14
models/constants.go Normal file
View File

@@ -0,0 +1,14 @@
package models
// Roles
const (
RoleUser = "user"
RoleAdmin = "admin"
)
// ID-Types
const (
IDTypeSHARE = "share"
IDTypeEXPENSE = "expense"
IDTypeUSER = "user"
)

8
models/dbmodels.go Normal file
View File

@@ -0,0 +1,8 @@
package models
type User struct {
ID string `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Role string `json:"role"`
}

11
models/loginmodels.go Normal file
View File

@@ -0,0 +1,11 @@
package models
type RefreshToken struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Token string `json:"token"`
ExpiresAt int64 `json:"expires_at"`
CreatedAt int64 `json:"created_at"`
Revoked bool `json:"revoked"`
DeviceInfo string `json:"device_info"`
}

78
server/server.go Normal file
View File

@@ -0,0 +1,78 @@
package server
import (
"MiauInv/config"
"MiauInv/frontend"
"MiauInv/handlers"
"log"
"net/http"
"os"
)
type Server struct {
Port string
JWTSecret []byte
DatabasePath string
CertificatePath string
PrivateKeyPath string
}
func InitServer() *Server {
err := config.CheckIfExists()
if err != nil {
log.Fatal(err)
return nil
}
cfg, err := config.LoadConfig()
if err != nil {
log.Fatal(err)
return nil
}
jwtSecret := os.Getenv("JWT_SECRET")
if jwtSecret == "" {
log.Fatal("JWT_SECRET environment variable not set.")
return nil
}
if len(jwtSecret) < 32 {
log.Fatal("JWT_SECRET must be at least 32 characters long.")
return nil
}
return &Server{
Port: cfg.Port,
JWTSecret: []byte(jwtSecret),
DatabasePath: cfg.DatabasePath,
CertificatePath: cfg.CertificatePath,
PrivateKeyPath: cfg.PrivateKeyPath,
}
}
func (this *Server) Run() {
log.Println("Starting server...")
mux := http.NewServeMux()
//
// FRONTEND
//
mux.HandleFunc("/", frontend.Home)
//
// API
//
// Public
mux.HandleFunc("/api/login", handlers.Login)
mux.HandleFunc("/api/register", handlers.Register)
mux.HandleFunc("/api/refresh", handlers.RefreshToken)
mux.HandleFunc("/api/logout", handlers.Logout)
// Login required
// Admin-only
log.Printf("Listening on port %s", this.Port)
log.Fatal(http.ListenAndServeTLS(":"+this.Port, this.CertificatePath, this.PrivateKeyPath, mux))
}

104
storage/storage.go Normal file
View File

@@ -0,0 +1,104 @@
package storage
import (
"MiauInv/models"
"database/sql"
"errors"
"log"
_ "github.com/glebarez/go-sqlite"
)
var ErrNotFound = sql.ErrNoRows
var DB *sql.DB
func InitDB(filepath string) error {
var err error
DB, err = sql.Open("sqlite", filepath)
if err != nil {
return err
}
schema := `
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
category TEXT,
total_quantity INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS item_allocations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
item_id INTEGER NOT NULL,
location_id INTEGER,
project_id INTEGER,
quantity INTEGER NOT NULL
);
`
_, err = DB.Exec(schema)
if err != nil {
log.Fatal(err)
}
return err
}
// Refresh Tokens
func AddRefreshToken(token *models.RefreshToken) error {
_, err := DB.Exec("INSERT INTO refresh_tokens(id, user_id, token_hash, expires_at, created_at, revoked, device_info) VALUES (?, ?, ?, ?, ?, ?, ?)",
token.ID, token.UserID, token.Token, token.ExpiresAt, token.CreatedAt, token.Revoked, token.DeviceInfo)
return err
}
func GetRefreshToken(token string) (models.RefreshToken, error) {
row := DB.QueryRow("SELECT * FROM refresh_tokens WHERE token_hash = ?", token)
var refresh_token models.RefreshToken
err := row.Scan(&refresh_token.ID, &refresh_token.UserID, &refresh_token.Token, &refresh_token.ExpiresAt, &refresh_token.CreatedAt, &refresh_token.Revoked, &refresh_token.DeviceInfo)
return refresh_token, err
}
func RevokeRefreshToken(tokenID string) error {
if DB == nil {
return errors.New("db not initialized")
}
res, err := DB.Exec(`
UPDATE refresh_tokens
SET revoked = 1
WHERE id = ?
`, tokenID)
if err != nil {
return err
}
n, err := res.RowsAffected()
if err != nil {
return err
}
if n == 0 {
return ErrNotFound
}
return nil
}
func RevokeAllRefreshTokensForUser(userID string) error {
if DB == nil {
return errors.New("db not initialized")
}
_, err := DB.Exec(`
UPDATE refresh_tokens
SET revoked = 1
WHERE user_id = ?
`, userID)
return err
}

49
util/util.go Normal file
View File

@@ -0,0 +1,49 @@
package utils
import (
"MiauInv/auth"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"net/http"
"github.com/google/uuid"
)
func GenerateUUID() string {
return uuid.New().String()
}
func GenerateSecret() string {
b := make([]byte, 64)
_, err := rand.Read(b)
if err != nil {
return err.Error()
}
return base64.StdEncoding.EncodeToString(b)
}
func GenerateRefreshToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
func HashToken(token string) string {
hash := sha256.Sum256([]byte(token))
return hex.EncodeToString(hash[:])
}
func IsLoggedIn(w http.ResponseWriter, r *http.Request) (*auth.Claims, bool) {
claimsRaw := r.Context().Value(auth.UserContextKey)
if claimsRaw == nil {
http.Error(w, "No claims in context", http.StatusUnauthorized)
return nil, false
}
claims, ok := claimsRaw.(*auth.Claims)
if !ok {
http.Error(w, "Invalid claims", http.StatusUnauthorized)
return nil, false
}
return claims, true
}