diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e61f38 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +users.db \ No newline at end of file diff --git a/go.mod b/go.mod index ba2998d..d981de2 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ module playback-device-server go 1.24.1 + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-sqlite3 v1.14.24 // indirect + golang.org/x/crypto v0.36.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c5d5573 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +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-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= diff --git a/main/main.go b/main/main.go index f7b60bd..8b3a7d7 100644 --- a/main/main.go +++ b/main/main.go @@ -1,7 +1,70 @@ package main -import "fmt" +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "os" + "playback-device-server/users" +) + +const DEFAULT_USERNAME = "admin" +const MIN_PASSWORD_LENGTH = 8 +const USER_DATABASE_DIR = "." func main() { - fmt.Println("Hello, world!") + userDatabase := users.UserDatabase{} + userDatabase.SetDirectory(USER_DATABASE_DIR) + err := userDatabase.Initialize() + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + defer userDatabase.Close() + + exists, error := userDatabase.UsernameExists(DEFAULT_USERNAME) + if error != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + if !exists { + password, error := generateRandomPassword(MIN_PASSWORD_LENGTH) + if error != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + fmt.Println() + fmt.Println("Creating default admin user:") + fmt.Printf("Username: %s\n", DEFAULT_USERNAME) + fmt.Printf("Password: %s\n", password) + fmt.Println() + + user := users.User{Username: DEFAULT_USERNAME, Password: password, IsAdmin: true} + _, error = userDatabase.CreateUser(user.Username, user.Password, user.IsAdmin) + if error != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + } + +} + +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 } diff --git a/start b/start index 097c109..3c658ad 100755 Binary files a/start and b/start differ diff --git a/users/session.go b/users/session.go new file mode 100644 index 0000000..35a269a --- /dev/null +++ b/users/session.go @@ -0,0 +1,9 @@ +package users + +import "time" + +type Session struct { + UserID string + Token string + ExpiryDate time.Time +} diff --git a/users/user.go b/users/user.go new file mode 100644 index 0000000..de00947 --- /dev/null +++ b/users/user.go @@ -0,0 +1,8 @@ +package users + +type User struct { + ID string `json:"id"` + Username string `json:"username"` + Password string `json:"password"` + IsAdmin bool `json:"is_admin"` +} diff --git a/users/user_database.go b/users/user_database.go new file mode 100644 index 0000000..5786fdf --- /dev/null +++ b/users/user_database.go @@ -0,0 +1,195 @@ +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 +}