Compare commits
No commits in common. "73a398b8e1345e8f328b36ae767d8c84c900514b" and "16d58d8202506553b7459c2db005a5f8f4a5ba1e" have entirely different histories.
73a398b8e1
...
16d58d8202
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
users.db
|
||||
start
|
||||
*.log
|
||||
10
go.mod
10
go.mod
@ -1,13 +1,3 @@
|
||||
module playback-device-server
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||
github.com/rs/zerolog v1.33.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
)
|
||||
|
||||
22
go.sum
22
go.sum
@ -1,22 +0,0 @@
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
41
main/main.go
41
main/main.go
@ -1,44 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"playback-device-server/users"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const USER_DATABASE_DIR = "."
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
initializeLogger()
|
||||
log.Info().Msg("starting playback device server")
|
||||
|
||||
userDatabase := users.UserDatabase{}
|
||||
userDatabase.SetDirectory(USER_DATABASE_DIR)
|
||||
err := userDatabase.Initialize()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to initialize user database")
|
||||
os.Exit(1)
|
||||
}
|
||||
defer userDatabase.Close()
|
||||
|
||||
userManager := users.UserManager{}
|
||||
err = userManager.Initialize(&userDatabase)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to initialize user manager")
|
||||
}
|
||||
}
|
||||
|
||||
func initializeLogger() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
||||
|
||||
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
log.Printf("failed to open log file: %v", err)
|
||||
}
|
||||
|
||||
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
|
||||
multi := zerolog.MultiLevelWriter(consoleWriter, file)
|
||||
log.Logger = zerolog.New(multi).With().Timestamp().Logger()
|
||||
fmt.Println("Hello, world!")
|
||||
}
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
package users
|
||||
|
||||
import "time"
|
||||
|
||||
type Session struct {
|
||||
UserID string
|
||||
Token string
|
||||
ExpiryDate time.Time
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
package users
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
}
|
||||
@ -1,195 +0,0 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type UserDatabase struct {
|
||||
Connection *sql.DB
|
||||
databaseDirectory string
|
||||
}
|
||||
|
||||
func (db *UserDatabase) Initialize() error {
|
||||
connection, error := sql.Open("sqlite3", filepath.Join(db.databaseDirectory, "users.db"))
|
||||
if error != nil {
|
||||
return error
|
||||
}
|
||||
db.Connection = connection
|
||||
|
||||
_, error = db.Connection.Exec(`CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT UNIQUE,
|
||||
password TEXT,
|
||||
is_admin INTEGER
|
||||
)`)
|
||||
if error != nil {
|
||||
return fmt.Errorf("error creating users table: %s", error)
|
||||
}
|
||||
|
||||
_, error = db.Connection.Exec(`CREATE TABLE IF NOT EXISTS sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
user_id INTEGER,
|
||||
expiry_date TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)`)
|
||||
if error != nil {
|
||||
return fmt.Errorf("error creating sessions table: %s", error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *UserDatabase) Close() error {
|
||||
return db.Connection.Close()
|
||||
}
|
||||
func (db *UserDatabase) CreateUser(username, password string, isAdmin bool) (string, error) {
|
||||
userID := uuid.New()
|
||||
hashedPassword, err := hashPassword(password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = db.Connection.Exec("INSERT INTO users (id, username, password, is_admin) VALUES (?, ?, ?, ?)", userID.String(), username, hashedPassword, isAdmin)
|
||||
return userID.String(), err
|
||||
}
|
||||
|
||||
func (db *UserDatabase) CreateSession(userID string, expiryDate time.Time) (string, error) {
|
||||
sessionToken := uuid.New()
|
||||
_, err := db.Connection.Exec("INSERT INTO sessions (user_id, token, expiry_date) VALUES (?, ?, ?)", userID, sessionToken.String(), expiryDate)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sessionToken.String(), nil
|
||||
}
|
||||
|
||||
func (db *UserDatabase) GetUserByUsername(username string) (*User, error) {
|
||||
var user User
|
||||
err := db.Connection.QueryRow("SELECT id, username, password, is_admin FROM users WHERE username = ?", username).Scan(&user.ID, &user.Username, &user.Password, &user.IsAdmin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (db *UserDatabase) GetUserById(id string) (*User, error) {
|
||||
var user User
|
||||
err := db.Connection.QueryRow("SELECT id, username, password, is_admin FROM users WHERE id = ?", id).Scan(&user.ID, &user.Username, &user.Password, &user.IsAdmin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (db *UserDatabase) UsernameExists(username string) (bool, error) {
|
||||
var exists bool
|
||||
err := db.Connection.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE username = ?)", username).Scan(&exists)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func (db *UserDatabase) UserIdExists(id string) (bool, error) {
|
||||
var exists bool
|
||||
err := db.Connection.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE id = ?)", id).Scan(&exists)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func (db *UserDatabase) UpdateUser(user *User) error {
|
||||
_, err := db.Connection.Exec("UPDATE users SET username = ?, is_admin = ? WHERE id = ?", user.Username, user.IsAdmin, user.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *UserDatabase) UpdatePassword(userId string, newPassword string) error {
|
||||
hashedPassword, err := hashPassword(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.Connection.Exec("UPDATE users SET password = ? WHERE id = ?", hashedPassword, userId)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *UserDatabase) CheckCredentials(username, password string) (bool, error) {
|
||||
var hashedPassword string
|
||||
err := db.Connection.QueryRow("SELECT password FROM users WHERE username = ?", username).Scan(&hashedPassword)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (db *UserDatabase) GetSession(sessionToken string) (*Session, error) {
|
||||
var session Session
|
||||
row := db.Connection.QueryRow("SELECT token, user_id, expiry_date FROM sessions WHERE token = ?", sessionToken)
|
||||
err := row.Scan(&session.Token, &session.UserID, &session.ExpiryDate)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
func (db *UserDatabase) DeleteSessionByToken(token string) error {
|
||||
_, err := db.Connection.Exec("DELETE FROM sessions WHERE token = ?", token)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *UserDatabase) GetUsers() (*[]User, error) {
|
||||
var users []User
|
||||
|
||||
rows, err := db.Connection.Query("SELECT id, username, is_admin FROM users")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var user User
|
||||
err := rows.Scan(&user.ID, &user.Username, &user.IsAdmin)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
return &users, nil
|
||||
}
|
||||
|
||||
func (db *UserDatabase) DeleteUser(ID string) error {
|
||||
_, err := db.Connection.Exec("DELETE FROM users WHERE id = ?", ID)
|
||||
return err
|
||||
}
|
||||
|
||||
func hashPassword(password string) (string, error) {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hashedPassword), nil
|
||||
}
|
||||
|
||||
func (db *UserDatabase) SetDirectory(directory string) {
|
||||
db.databaseDirectory = directory
|
||||
}
|
||||
@ -1,190 +0,0 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const DEFAULT_USERNAME = "admin"
|
||||
const MIN_PASSWORD_LENGTH = 6
|
||||
|
||||
type UserManager struct {
|
||||
userDatabase *UserDatabase
|
||||
}
|
||||
|
||||
func (um *UserManager) Initialize(userDatabase *UserDatabase) error {
|
||||
um.userDatabase = userDatabase
|
||||
|
||||
exists, error := um.UsernameExists(DEFAULT_USERNAME)
|
||||
if error != nil {
|
||||
return error
|
||||
}
|
||||
|
||||
if !exists {
|
||||
password, error := generateRandomPassword(MIN_PASSWORD_LENGTH)
|
||||
if error != nil {
|
||||
return error
|
||||
}
|
||||
|
||||
log.Info().Str("username", DEFAULT_USERNAME).Str("password", password).Msg("creating default admin user")
|
||||
|
||||
user := User{Username: DEFAULT_USERNAME, Password: password, IsAdmin: true}
|
||||
_, error = um.CreateUser(&user)
|
||||
if error != nil {
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (um *UserManager) CreateUser(user *User) (string, error) {
|
||||
exists, error := um.UsernameExists(user.Username)
|
||||
if error != nil {
|
||||
return "", error
|
||||
}
|
||||
if exists {
|
||||
return "", fmt.Errorf("User '%s' already exists", user.Username)
|
||||
}
|
||||
|
||||
if !isValidPassword(user.Password) {
|
||||
return "", fmt.Errorf("invalid password")
|
||||
}
|
||||
|
||||
id, error := um.userDatabase.CreateUser(user.Username, user.Password, user.IsAdmin)
|
||||
if error != nil {
|
||||
return "", error
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (um *UserManager) UsernameExists(username string) (bool, error) {
|
||||
exists, error := um.userDatabase.UsernameExists(username)
|
||||
if error != nil {
|
||||
return false, error
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func (um *UserManager) UserIdExists(id string) (bool, error) {
|
||||
exists, error := um.userDatabase.UserIdExists(id)
|
||||
if error != nil {
|
||||
return false, error
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func (um *UserManager) Login(username, password string) (string, error) {
|
||||
exists, error := um.UsernameExists(username)
|
||||
if error != nil {
|
||||
return "", error
|
||||
}
|
||||
if !exists {
|
||||
return "", fmt.Errorf("user '%s' doesn't exist", username)
|
||||
}
|
||||
|
||||
correct, error := um.userDatabase.CheckCredentials(username, password)
|
||||
if error != nil {
|
||||
return "", error
|
||||
}
|
||||
if !correct {
|
||||
return "", fmt.Errorf("wrong password")
|
||||
}
|
||||
|
||||
user, error := um.userDatabase.GetUserByUsername(username)
|
||||
if error != nil {
|
||||
return "", error
|
||||
}
|
||||
|
||||
expiryDate := time.Now().AddDate(0, 0, 30)
|
||||
token, error := um.userDatabase.CreateSession(user.ID, expiryDate)
|
||||
if error != nil {
|
||||
return "", error
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (um *UserManager) GetSession(sessionToken string) (*Session, error) {
|
||||
session, error := um.userDatabase.GetSession(sessionToken)
|
||||
if error != nil {
|
||||
return nil, error
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (um *UserManager) GetUserById(id string) (*User, error) {
|
||||
user, error := um.userDatabase.GetUserById(id)
|
||||
if error != nil {
|
||||
return nil, error
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (um *UserManager) UpdateUser(user *User) error {
|
||||
error := um.userDatabase.UpdateUser(user)
|
||||
return error
|
||||
}
|
||||
|
||||
func (um *UserManager) UpdatePassword(currentPassword string, newPassword string, user *User) error {
|
||||
correct, error := um.userDatabase.CheckCredentials(user.Username, currentPassword)
|
||||
if error != nil {
|
||||
return error
|
||||
}
|
||||
if !correct {
|
||||
return fmt.Errorf("wrong password")
|
||||
}
|
||||
|
||||
if !isValidPassword(user.Password) {
|
||||
return fmt.Errorf("invalid password")
|
||||
}
|
||||
|
||||
error = um.userDatabase.UpdatePassword(user.ID, newPassword)
|
||||
return error
|
||||
}
|
||||
|
||||
func (um *UserManager) DeleteSession(token string) error {
|
||||
error := um.userDatabase.DeleteSessionByToken(token)
|
||||
if error != nil {
|
||||
return error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (um *UserManager) GetUsers() (*[]User, error) {
|
||||
users, error := um.userDatabase.GetUsers()
|
||||
|
||||
return users, error
|
||||
}
|
||||
|
||||
func (um *UserManager) DeleteUser(ID string) error {
|
||||
error := um.userDatabase.DeleteUser(ID)
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
func isValidPassword(password string) bool {
|
||||
return len(password) >= MIN_PASSWORD_LENGTH
|
||||
}
|
||||
|
||||
func generateRandomPassword(length int) (string, error) {
|
||||
numBytes := length * 3 / 4 // Base64 encoding increases length by 4/3
|
||||
|
||||
randomBytes := make([]byte, numBytes)
|
||||
_, err := rand.Read(randomBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
password := base64.URLEncoding.EncodeToString(randomBytes)
|
||||
|
||||
if len(password) > length {
|
||||
password = password[:length]
|
||||
}
|
||||
|
||||
return password, nil
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user