Refactor everything again
This commit is contained in:
parent
7fdb0bf0f4
commit
96c369e4b1
14 changed files with 72 additions and 53 deletions
40
internal/app/find.go
Normal file
40
internal/app/find.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.a71.su/Andrew71/pye/internal/models/user"
|
||||
"git.a71.su/Andrew71/pye/internal/storage"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(findUserCmd)
|
||||
}
|
||||
|
||||
var findUserCmd = &cobra.Command{
|
||||
Use: "find <uuid/email> <query>",
|
||||
Short: "Find a user",
|
||||
Long: `Find information about a user from their UUID or email`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: findUser,
|
||||
}
|
||||
|
||||
func findUser(cmd *cobra.Command, args []string) {
|
||||
var user user.User
|
||||
var ok bool
|
||||
if args[0] == "email" {
|
||||
user, ok = storage.Data.ByEmail(args[1])
|
||||
} else if args[0] == "uuid" {
|
||||
user, ok = storage.Data.ById(args[1])
|
||||
} else {
|
||||
fmt.Println("Expected email or uuid")
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
fmt.Println("User not found")
|
||||
} else {
|
||||
fmt.Printf("Information for user:\nuuid\t- %s\nemail\t- %s\nhash\t- %s\n",
|
||||
user.Uuid, user.Email, user.Hash)
|
||||
}
|
||||
}
|
50
internal/app/root.go
Normal file
50
internal/app/root.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.a71.su/Andrew71/pye/internal/auth"
|
||||
"git.a71.su/Andrew71/pye/internal/config"
|
||||
"git.a71.su/Andrew71/pye/internal/logging"
|
||||
"git.a71.su/Andrew71/pye/internal/storage"
|
||||
"git.a71.su/Andrew71/pye/internal/storage/sqlite"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "pye",
|
||||
Short: "Pye is a simple JWT system",
|
||||
Long: `A bare-bones authentication system with RS256`,
|
||||
}
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
cfgDb string
|
||||
debugMode *bool
|
||||
)
|
||||
|
||||
func initConfig() {
|
||||
logging.Load(*debugMode)
|
||||
config.MustLoad(cfgFile)
|
||||
if cfgDb != "" {
|
||||
config.Cfg.SQLiteFile = cfgDb
|
||||
}
|
||||
|
||||
auth.MustLoadKey()
|
||||
storage.Data = sqlite.MustLoad(config.Cfg.SQLiteFile)
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "config.json", "config file")
|
||||
rootCmd.PersistentFlags().StringVar(&cfgDb, "db", "", "database to use")
|
||||
debugMode = rootCmd.PersistentFlags().BoolP("debug", "d", false, "enable debug mode")
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
48
internal/app/serve.go
Normal file
48
internal/app/serve.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.a71.su/Andrew71/pye/internal/auth"
|
||||
"git.a71.su/Andrew71/pye/internal/config"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var port int
|
||||
|
||||
func init() {
|
||||
serveCmd.Flags().IntVarP(&port, "port", "p", 0, "port to use")
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Start JWT service",
|
||||
Long: `Start a simple authentication service`,
|
||||
Run: serveAuth,
|
||||
}
|
||||
|
||||
func serveAuth(cmd *cobra.Command, args []string) {
|
||||
if port == 0 {
|
||||
port = config.Cfg.Port
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Logger, middleware.CleanPath, middleware.StripSlashes)
|
||||
|
||||
r.Get("/pem", auth.ServePublicKey)
|
||||
r.Post("/register", auth.Register)
|
||||
r.Post("/login", auth.Login)
|
||||
|
||||
// Note: likely temporary, possibly to be replaced by a fake "frontend"
|
||||
r.Get("/register", auth.Register)
|
||||
r.Get("/login", auth.Login)
|
||||
|
||||
slog.Info("🪐 pye started", "port", port)
|
||||
http.ListenAndServe(":"+strconv.Itoa(port), r)
|
||||
}
|
58
internal/app/verify.go
Normal file
58
internal/app/verify.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"git.a71.su/Andrew71/pye/internal/auth"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
verifyToken string
|
||||
verifyFile string
|
||||
)
|
||||
|
||||
func init() {
|
||||
verifyCmd.Flags().StringVarP(&verifyToken, "token", "t", "", "token to verify")
|
||||
verifyCmd.MarkFlagRequired("token")
|
||||
verifyCmd.Flags().StringVarP(&verifyFile, "file", "f", "", "PEM file to use")
|
||||
rootCmd.AddCommand(verifyCmd)
|
||||
}
|
||||
|
||||
var verifyCmd = &cobra.Command{
|
||||
Use: "verify",
|
||||
Short: "Verify a JWT token",
|
||||
Long: `Pass a JWT token (and optionally a path to a PEM-formatted file with the public key)
|
||||
to verify whether it is valid.`,
|
||||
Run: verifyFunc,
|
||||
}
|
||||
|
||||
func verifyFunc(cmd *cobra.Command, args []string) {
|
||||
if verifyToken == "" {
|
||||
fmt.Println("Empty token supplied!")
|
||||
return
|
||||
}
|
||||
|
||||
var t *jwt.Token
|
||||
var err error
|
||||
if verifyFile == "" {
|
||||
fmt.Println("No PEM file supplied, assuming local")
|
||||
t, err = auth.VerifyLocal(verifyToken)
|
||||
} else {
|
||||
key, err_k := os.ReadFile(verifyFile)
|
||||
if err_k != nil {
|
||||
slog.Error("error reading file", "error", err, "file", verifyFile)
|
||||
return
|
||||
}
|
||||
t, err = auth.Verify(verifyToken, key)
|
||||
}
|
||||
slog.Debug("result", "token", t, "error", err, "ok", err == nil)
|
||||
if err == nil {
|
||||
fmt.Println("Token valid!")
|
||||
} else {
|
||||
fmt.Println("Token invalid!", err)
|
||||
}
|
||||
}
|
82
internal/auth/auth.go
Normal file
82
internal/auth/auth.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"git.a71.su/Andrew71/pye/internal/storage"
|
||||
)
|
||||
|
||||
func validEmail(email string) bool {
|
||||
_, err := mail.ParseAddress(email)
|
||||
return err == nil
|
||||
}
|
||||
func validPass(pass string) bool {
|
||||
// TODO: Obviously, we *might* want something more sophisticated here
|
||||
return len(pass) >= 8
|
||||
}
|
||||
|
||||
// Register creates a new user with credentials provided through Basic Auth
|
||||
func Register(w http.ResponseWriter, r *http.Request) {
|
||||
email, password, ok := r.BasicAuth()
|
||||
|
||||
if ok {
|
||||
email = strings.TrimSpace(email)
|
||||
password = strings.TrimSpace(password)
|
||||
if !(validEmail(email) && validPass(password)) {
|
||||
slog.Debug("outcome",
|
||||
"valid_email", validEmail(email),
|
||||
"valid_pass", validPass(password),
|
||||
"taken_email", storage.Data.Taken(email))
|
||||
http.Error(w, "invalid auth credentials", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := storage.Data.Add(email, password)
|
||||
if err != nil {
|
||||
if errors.Is(err, storage.ErrExist) {
|
||||
http.Error(w, "invalid auth credentials", http.StatusBadRequest)
|
||||
} else {
|
||||
http.Error(w, "error adding a new user", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write([]byte("user created"))
|
||||
return
|
||||
}
|
||||
|
||||
// No email and password was provided
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
http.Error(w, "This API requires authorization", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
// Login returns JWT for a registered user through Basic Auth
|
||||
func Login(w http.ResponseWriter, r *http.Request) {
|
||||
email, password, ok := r.BasicAuth()
|
||||
|
||||
if ok {
|
||||
email = strings.TrimSpace(email)
|
||||
password = strings.TrimSpace(password)
|
||||
user, ok := storage.Data.ByEmail(email)
|
||||
if !ok || !user.Fits(password) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
http.Error(w, "you did something wrong", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := Create(user)
|
||||
if err != nil {
|
||||
http.Error(w, "error creating jwt", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Write([]byte(token))
|
||||
return
|
||||
}
|
||||
|
||||
// No email and password was provided
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
http.Error(w, "This API requires authorization", http.StatusUnauthorized)
|
||||
}
|
107
internal/auth/jwt.go
Normal file
107
internal/auth/jwt.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.a71.su/Andrew71/pye/internal/config"
|
||||
"git.a71.su/Andrew71/pye/internal/models/user"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var key *rsa.PrivateKey
|
||||
|
||||
// LoadKey attempts to load a private RS256 key from file.
|
||||
// If the file does not exist, it generates a new key (and saves it)
|
||||
func MustLoadKey() {
|
||||
// If the key doesn't exist, create it
|
||||
if _, err := os.Stat(config.Cfg.KeyFile); errors.Is(err, os.ErrNotExist) {
|
||||
key, err = rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
slog.Error("error generating key", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Save key to disk
|
||||
km := x509.MarshalPKCS1PrivateKey(key)
|
||||
block := pem.Block{Bytes: km, Type: "RSA PRIVATE KEY"}
|
||||
f, err := os.OpenFile(config.Cfg.KeyFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
slog.Error("error opening/creating file", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
f.Write(pem.EncodeToMemory(&block))
|
||||
if err := f.Close(); err != nil {
|
||||
slog.Error("error closing file", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
slog.Debug("generated new key", "file", config.Cfg.KeyFile)
|
||||
} else {
|
||||
km, err := os.ReadFile(config.Cfg.KeyFile)
|
||||
if err != nil {
|
||||
slog.Error("error reading key", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
key, err = jwt.ParseRSAPrivateKeyFromPEM(km)
|
||||
if err != nil {
|
||||
slog.Error("error parsing key", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
slog.Debug("loaded private key", "file", config.Cfg.KeyFile)
|
||||
}
|
||||
}
|
||||
|
||||
// ServePublicKey returns our public key as PEM block over HTTP
|
||||
func ServePublicKey(w http.ResponseWriter, r *http.Request) {
|
||||
key_marshalled := x509.MarshalPKCS1PublicKey(&key.PublicKey)
|
||||
block := pem.Block{Bytes: key_marshalled, Type: "RSA PUBLIC KEY"}
|
||||
pem.Encode(w, &block)
|
||||
}
|
||||
|
||||
// Create creates a JSON Web Token that expires after a week
|
||||
func Create(user user.User) (token string, err error) {
|
||||
t := jwt.NewWithClaims(jwt.SigningMethodRS256,
|
||||
jwt.MapClaims{
|
||||
"iss": "pye",
|
||||
"uid": user.Uuid,
|
||||
"sub": user.Email,
|
||||
"iat": time.Now().Unix(),
|
||||
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(),
|
||||
})
|
||||
token, err = t.SignedString(key)
|
||||
if err != nil {
|
||||
slog.Error("error creating JWT", "error", err)
|
||||
return "", err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Verify receives a JWT and PEM-encoded public key,
|
||||
// then returns whether the token is valid
|
||||
func Verify(token string, publicKey []byte) (*jwt.Token, error) {
|
||||
t, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
||||
key, err := jwt.ParseRSAPublicKeyFromPEM(publicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, err
|
||||
}
|
||||
return key, nil
|
||||
})
|
||||
return t, err
|
||||
}
|
||||
|
||||
// VerifyLocal calls Verify with public key set to current local one
|
||||
func VerifyLocal(token string) (*jwt.Token, error) {
|
||||
key_marshalled := x509.MarshalPKCS1PublicKey(&key.PublicKey)
|
||||
block := pem.Block{Bytes: key_marshalled, Type: "RSA PUBLIC KEY"}
|
||||
return Verify(token, pem.EncodeToMemory(&block))
|
||||
}
|
54
internal/config/config.go
Normal file
54
internal/config/config.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Config stores app configuration
|
||||
type Config struct {
|
||||
Port int `json:"port"`
|
||||
KeyFile string `json:"key-file"`
|
||||
SQLiteFile string `json:"sqlite-file"`
|
||||
LogToFile bool `json:"log-to-file"`
|
||||
LogFile string `json:"log-file"`
|
||||
}
|
||||
|
||||
var DefaultConfig = Config{
|
||||
Port: 7102,
|
||||
KeyFile: "private.key",
|
||||
SQLiteFile: "data.db",
|
||||
LogToFile: false,
|
||||
LogFile: "pye.log",
|
||||
}
|
||||
|
||||
var (
|
||||
DefaultLocation = "config.json"
|
||||
Cfg Config
|
||||
)
|
||||
|
||||
// Load parses a JSON-formatted configuration file
|
||||
func load(filename string) (Config, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
conf := DefaultConfig
|
||||
err = json.Unmarshal(data, &conf)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
slog.Debug("Loaded config", "file", filename, "config", conf)
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// MustLoad handles initial configuration loading
|
||||
func MustLoad(filename string) {
|
||||
conf, err := load(filename)
|
||||
if err != nil {
|
||||
slog.Error("error initially loading config", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
Cfg = conf
|
||||
}
|
36
internal/logging/log.go
Normal file
36
internal/logging/log.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"git.a71.su/Andrew71/pye/internal/config"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
)
|
||||
|
||||
// Load makes slog output to both os.Stdout and a file if needed, and sets slog.LevelDebug if enabled.
|
||||
func Load(debugMode bool) {
|
||||
var w io.Writer
|
||||
if config.Cfg.LogToFile {
|
||||
f, err := os.OpenFile(config.Cfg.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
slog.Error("error opening log file, logging to stdout only", "path", config.Cfg.LogFile, "error", err)
|
||||
return
|
||||
}
|
||||
// No defer f.Close() because that breaks the MultiWriter
|
||||
w = io.MultiWriter(f, os.Stdout)
|
||||
} else {
|
||||
w = os.Stdout
|
||||
}
|
||||
|
||||
// Make slog use intended format
|
||||
var opts *slog.HandlerOptions
|
||||
if debugMode {
|
||||
opts = &slog.HandlerOptions{Level: slog.LevelDebug}
|
||||
}
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(w, opts)))
|
||||
middleware.DefaultLogger = middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: log.Default(), NoColor: true})
|
||||
slog.Debug("debug mode active")
|
||||
}
|
30
internal/models/user/user.go
Normal file
30
internal/models/user/user.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Uuid uuid.UUID
|
||||
Email string
|
||||
Hash []byte // bcrypt hash of password
|
||||
}
|
||||
|
||||
// Fits checks whether the password is correct
|
||||
func (u User) Fits(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword(u.Hash, []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// New Creates a new User
|
||||
func New(email, password string) (User, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
if err != nil {
|
||||
slog.Error("error creating a new user", "error", err)
|
||||
return User{}, err
|
||||
}
|
||||
return User{uuid.New(), email, hash}, nil
|
||||
}
|
92
internal/storage/sqlite/sqlite.go
Normal file
92
internal/storage/sqlite/sqlite.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"git.a71.su/Andrew71/pye/internal/models/user"
|
||||
"git.a71.su/Andrew71/pye/internal/storage"
|
||||
sqlite "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
const create string = `
|
||||
CREATE TABLE "users" (
|
||||
"uuid" TEXT NOT NULL UNIQUE,
|
||||
"email" TEXT NOT NULL UNIQUE,
|
||||
"password" TEXT NOT NULL,
|
||||
PRIMARY KEY("uuid")
|
||||
);`
|
||||
|
||||
// SQLiteStorage is a Storage implementation with SQLite3
|
||||
type SQLiteStorage struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (s SQLiteStorage) Add(email, password string) error {
|
||||
user, err := user.New(email, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.db.Exec("insert into users (uuid, email, password) values ($1, $2, $3)",
|
||||
user.Uuid.String(), user.Email, user.Hash)
|
||||
if err != nil {
|
||||
e, ok := err.(sqlite.Error)
|
||||
if ok && errors.Is(e.ExtendedCode, sqlite.ErrConstraintUnique) {
|
||||
// Return a standard error if the user already exists
|
||||
slog.Info("can't add user because email already taken", "user", user)
|
||||
return fmt.Errorf("%w (%s)", storage.ErrExist, user.Email)
|
||||
}
|
||||
slog.Error("error adding user to database", "error", err, "user", user)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SQLiteStorage) ById(uuid string) (user.User, bool) {
|
||||
row := s.db.QueryRow("select * from users where uuid = $1", uuid)
|
||||
user := user.User{}
|
||||
err := row.Scan(&user.Uuid, &user.Email, &user.Hash)
|
||||
|
||||
return user, err == nil
|
||||
}
|
||||
|
||||
func (s SQLiteStorage) ByEmail(email string) (user.User, bool) {
|
||||
row := s.db.QueryRow("select * from users where email = $1", email)
|
||||
user := user.User{}
|
||||
err := row.Scan(&user.Uuid, &user.Email, &user.Hash)
|
||||
|
||||
return user, err == nil
|
||||
}
|
||||
|
||||
func (s SQLiteStorage) Taken(email string) bool {
|
||||
_, ok := s.ByEmail(email)
|
||||
return ok
|
||||
}
|
||||
|
||||
func MustLoad(dataFile string) SQLiteStorage {
|
||||
if _, err := os.Stat(dataFile); errors.Is(err, os.ErrNotExist) {
|
||||
os.Create(dataFile)
|
||||
slog.Debug("created sqlite3 database file", "file", dataFile)
|
||||
}
|
||||
db, err := sql.Open("sqlite3", dataFile)
|
||||
if err != nil {
|
||||
slog.Error("error opening sqlite3 database", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
statement, err := db.Prepare(create)
|
||||
if err != nil {
|
||||
if err.Error() != "table \"users\" already exists" {
|
||||
slog.Error("error initialising sqlite3 database table", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
statement.Exec()
|
||||
}
|
||||
|
||||
slog.Debug("loaded database", "file", dataFile)
|
||||
return SQLiteStorage{db}
|
||||
}
|
21
internal/storage/storage.go
Normal file
21
internal/storage/storage.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.a71.su/Andrew71/pye/internal/models/user"
|
||||
)
|
||||
|
||||
var ErrExist = errors.New("user already exists")
|
||||
|
||||
// Storage is an interface for arbitrary storage
|
||||
type Storage interface {
|
||||
Add(email, password string) error // Add inserts a user into data
|
||||
ById(uuid string) (user.User, bool) // ById retrieves a user by their UUID
|
||||
ByEmail(email string) (user.User, bool) // ByEmail retrieves a user by their email
|
||||
Taken(email string) bool // Taken checks whether an email is taken
|
||||
}
|
||||
|
||||
// Data stores active information for the app.
|
||||
// It should be populated on startup
|
||||
var Data Storage
|
Loading…
Add table
Add a link
Reference in a new issue