Compare commits
2 Commits
e845d88b6c
...
ca5b14fc13
| Author | SHA1 | Date | |
|---|---|---|---|
| ca5b14fc13 | |||
| 947ff566d1 |
@ -1,4 +1,4 @@
|
||||
package users
|
||||
package data
|
||||
|
||||
import "time"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package users
|
||||
package data
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
@ -1,4 +1,4 @@
|
||||
package users
|
||||
package data
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
15
main/main.go
15
main/main.go
@ -2,8 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"playback-device-server/data"
|
||||
"playback-device-server/management"
|
||||
"playback-device-server/server"
|
||||
"playback-device-server/users"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
@ -20,7 +21,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
userDatabase := users.UserDatabase{}
|
||||
userDatabase := data.UserDatabase{}
|
||||
userDatabase.SetDirectory(configuration.DatabaseDirectory)
|
||||
err = userDatabase.Initialize()
|
||||
if err != nil {
|
||||
@ -29,7 +30,7 @@ func main() {
|
||||
}
|
||||
defer userDatabase.Close()
|
||||
|
||||
userManager := users.UserManager{}
|
||||
userManager := management.UserManager{}
|
||||
err = userManager.Initialize(&userDatabase)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to initialize user manager")
|
||||
@ -41,6 +42,14 @@ func main() {
|
||||
webServer.Initialize()
|
||||
defer webServer.Close()
|
||||
|
||||
authenticator := server.Authenticator{}
|
||||
authenticator.SetUserManager(&userManager)
|
||||
|
||||
userApiHandler := server.UsersApiHandler{}
|
||||
userApiHandler.SetUserManager(&userManager)
|
||||
userApiHandler.SetRouter(webServer.Router())
|
||||
userApiHandler.Initialize(&authenticator)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
package users
|
||||
package management
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
d "playback-device-server/data"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
@ -13,10 +14,10 @@ const DEFAULT_USERNAME = "admin"
|
||||
const MIN_PASSWORD_LENGTH = 6
|
||||
|
||||
type UserManager struct {
|
||||
userDatabase *UserDatabase
|
||||
userDatabase *d.UserDatabase
|
||||
}
|
||||
|
||||
func (um *UserManager) Initialize(userDatabase *UserDatabase) error {
|
||||
func (um *UserManager) Initialize(userDatabase *d.UserDatabase) error {
|
||||
um.userDatabase = userDatabase
|
||||
|
||||
exists, error := um.UsernameExists(DEFAULT_USERNAME)
|
||||
@ -32,7 +33,7 @@ func (um *UserManager) Initialize(userDatabase *UserDatabase) error {
|
||||
|
||||
log.Info().Str("username", DEFAULT_USERNAME).Str("password", password).Msg("creating default admin user")
|
||||
|
||||
user := User{Username: DEFAULT_USERNAME, Password: password, IsAdmin: true}
|
||||
user := d.User{Username: DEFAULT_USERNAME, Password: password, IsAdmin: true}
|
||||
_, error = um.CreateUser(&user)
|
||||
if error != nil {
|
||||
return error
|
||||
@ -42,7 +43,7 @@ func (um *UserManager) Initialize(userDatabase *UserDatabase) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (um *UserManager) CreateUser(user *User) (string, error) {
|
||||
func (um *UserManager) CreateUser(user *d.User) (string, error) {
|
||||
exists, error := um.UsernameExists(user.Username)
|
||||
if error != nil {
|
||||
return "", error
|
||||
@ -109,7 +110,7 @@ func (um *UserManager) Login(username, password string) (string, error) {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (um *UserManager) GetSession(sessionToken string) (*Session, error) {
|
||||
func (um *UserManager) GetSession(sessionToken string) (*d.Session, error) {
|
||||
session, error := um.userDatabase.GetSession(sessionToken)
|
||||
if error != nil {
|
||||
return nil, error
|
||||
@ -117,7 +118,7 @@ func (um *UserManager) GetSession(sessionToken string) (*Session, error) {
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (um *UserManager) GetUserById(id string) (*User, error) {
|
||||
func (um *UserManager) GetUserById(id string) (*d.User, error) {
|
||||
user, error := um.userDatabase.GetUserById(id)
|
||||
if error != nil {
|
||||
return nil, error
|
||||
@ -125,12 +126,12 @@ func (um *UserManager) GetUserById(id string) (*User, error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (um *UserManager) UpdateUser(user *User) error {
|
||||
func (um *UserManager) UpdateUser(user *d.User) error {
|
||||
error := um.userDatabase.UpdateUser(user)
|
||||
return error
|
||||
}
|
||||
|
||||
func (um *UserManager) UpdatePassword(currentPassword string, newPassword string, user *User) error {
|
||||
func (um *UserManager) UpdatePassword(currentPassword string, newPassword string, user *d.User) error {
|
||||
correct, error := um.userDatabase.CheckCredentials(user.Username, currentPassword)
|
||||
if error != nil {
|
||||
return error
|
||||
@ -155,7 +156,7 @@ func (um *UserManager) DeleteSession(token string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (um *UserManager) GetUsers() (*[]User, error) {
|
||||
func (um *UserManager) GetUsers() (*[]d.User, error) {
|
||||
users, error := um.userDatabase.GetUsers()
|
||||
|
||||
return users, error
|
||||
66
server/authenticator.go
Normal file
66
server/authenticator.go
Normal file
@ -0,0 +1,66 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
d "playback-device-server/data"
|
||||
m "playback-device-server/management"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type AuthContext struct {
|
||||
echo.Context
|
||||
User *d.User
|
||||
Session *d.Session
|
||||
}
|
||||
|
||||
type Authenticator struct {
|
||||
userManager *m.UserManager
|
||||
}
|
||||
|
||||
func (r *Authenticator) SetUserManager(userManager *m.UserManager) {
|
||||
r.userManager = userManager
|
||||
}
|
||||
|
||||
func (r *Authenticator) Authenticate(path string, exceptions []string) func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(context echo.Context) error {
|
||||
requestURI := context.Request().RequestURI
|
||||
if !strings.HasPrefix(requestURI, path) {
|
||||
return next(context)
|
||||
}
|
||||
for _, exception := range exceptions {
|
||||
if strings.HasPrefix(requestURI, exception) {
|
||||
return next(context)
|
||||
}
|
||||
}
|
||||
cookie, err := context.Cookie("token")
|
||||
if err != nil {
|
||||
SendError(401, context, "no session token found")
|
||||
return err
|
||||
}
|
||||
session, error := r.userManager.GetSession(cookie.Value)
|
||||
if error != nil {
|
||||
SendError(401, context, fmt.Sprintf("session not found: %s", cookie.Value))
|
||||
return fmt.Errorf("session not found: %s", cookie.Value)
|
||||
}
|
||||
|
||||
user, error := r.userManager.GetUserById(session.UserID)
|
||||
if error != nil {
|
||||
log.Error().Err(error).Msg("error getting user by id")
|
||||
SendError(401, context, "no user found for given session")
|
||||
return error
|
||||
}
|
||||
if user == nil {
|
||||
SendError(401, context, "no user found for given session")
|
||||
return fmt.Errorf("no user found for session '%s'", cookie.Value)
|
||||
}
|
||||
|
||||
authContext := AuthContext{Context: context, User: user, Session: session}
|
||||
|
||||
return next(authContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
262
server/user_api_handler.go
Normal file
262
server/user_api_handler.go
Normal file
@ -0,0 +1,262 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
d "playback-device-server/data"
|
||||
m "playback-device-server/management"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type UsersApiHandler struct {
|
||||
router *echo.Echo
|
||||
userManager *m.UserManager
|
||||
}
|
||||
|
||||
func (r *UsersApiHandler) Initialize(authenticator *Authenticator) {
|
||||
r.router.Use(authenticator.Authenticate("/api/users", []string{"/api/users/login"}))
|
||||
usersApi := r.router.Group("/api/users")
|
||||
usersApi.POST("/login", r.handleLogin)
|
||||
usersApi.POST("/logout", r.handleLogout)
|
||||
usersApi.GET("/session/info", r.handleGetSessionInfo)
|
||||
usersApi.POST("/newpassword", r.handleChangePassword)
|
||||
usersApi.PUT("/:id", r.handleUpdateConfig)
|
||||
usersApi.GET("", r.handleGetUsers)
|
||||
usersApi.POST("", r.handleCreateUser)
|
||||
usersApi.DELETE("/:id", r.handleDeleteUser)
|
||||
}
|
||||
|
||||
func (r UsersApiHandler) handleLogin(context echo.Context) error {
|
||||
var loginData struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
var errorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
error := context.Bind(&loginData)
|
||||
|
||||
if error != nil {
|
||||
log.Info().Msgf("failed to bind login data: %s", error)
|
||||
errorResponse.Error = fmt.Sprintf("%s", error)
|
||||
context.JSON(400, errorResponse)
|
||||
return error
|
||||
}
|
||||
|
||||
token, error := r.userManager.Login(loginData.Username, loginData.Password)
|
||||
|
||||
if error != nil {
|
||||
log.Info().Msgf("failed to login: %s", error)
|
||||
errorResponse.Error = fmt.Sprintf("%s", error)
|
||||
context.JSON(400, errorResponse)
|
||||
return error
|
||||
}
|
||||
|
||||
thirdyDays := 30 * 24 * 60 * 60
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: "token",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
MaxAge: thirdyDays,
|
||||
}
|
||||
|
||||
context.SetCookie(cookie)
|
||||
|
||||
return context.JSON(200, "")
|
||||
}
|
||||
|
||||
func (r UsersApiHandler) handleLogout(context echo.Context) error {
|
||||
r.removeTokenCookie(&context)
|
||||
context.JSON(200, "")
|
||||
|
||||
authContext := context.(AuthContext)
|
||||
|
||||
session := authContext.Session
|
||||
r.userManager.DeleteSession(session.Token)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r UsersApiHandler) handleChangePassword(context echo.Context) error {
|
||||
authContext := context.(AuthContext)
|
||||
user := authContext.User
|
||||
|
||||
var data struct {
|
||||
CurrentPassword string `json:"currentPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
|
||||
error := context.Bind(&data)
|
||||
|
||||
if error != nil {
|
||||
SendError(500, context, fmt.Sprintf("Failed to change password: %s", error))
|
||||
return error
|
||||
}
|
||||
|
||||
error = r.userManager.UpdatePassword(data.CurrentPassword, data.NewPassword, user)
|
||||
|
||||
if error != nil {
|
||||
SendError(500, context, fmt.Sprintf("Failed to change password: %s", error))
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
func (r UsersApiHandler) handleGetSessionInfo(context echo.Context) error {
|
||||
authContext := context.(AuthContext)
|
||||
user := authContext.User
|
||||
|
||||
response := struct {
|
||||
UserId string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
}{
|
||||
UserId: user.ID,
|
||||
Username: user.Username,
|
||||
IsAdmin: user.IsAdmin,
|
||||
}
|
||||
|
||||
return context.JSON(200, response)
|
||||
}
|
||||
|
||||
func (r UsersApiHandler) handleGetUsers(context echo.Context) error {
|
||||
users, error := r.userManager.GetUsers()
|
||||
if error != nil {
|
||||
SendError(500, context, fmt.Sprintf("failed to get user list: %s", error))
|
||||
}
|
||||
|
||||
return context.JSON(200, users)
|
||||
}
|
||||
|
||||
func (r UsersApiHandler) handleCreateUser(context echo.Context) error {
|
||||
authContext := context.(AuthContext)
|
||||
user := authContext.User
|
||||
|
||||
if !user.IsAdmin {
|
||||
SendError(403, context, "permission denied")
|
||||
log.Info().Msg("failed to delete user: permission denied")
|
||||
return fmt.Errorf("permission denied")
|
||||
}
|
||||
|
||||
var newUser d.User
|
||||
error := context.Bind(&newUser)
|
||||
|
||||
if error != nil {
|
||||
SendError(500, context, fmt.Sprintf("Failed to create user: %s", error))
|
||||
return error
|
||||
}
|
||||
|
||||
id, error := r.userManager.CreateUser(&newUser)
|
||||
|
||||
if error != nil {
|
||||
SendError(500, context, fmt.Sprintf("Failed to create user: %s", error))
|
||||
return error
|
||||
}
|
||||
|
||||
user.ID = id
|
||||
|
||||
return context.JSON(200, user)
|
||||
}
|
||||
|
||||
func (r UsersApiHandler) handleDeleteUser(context echo.Context) error {
|
||||
authContext := context.(AuthContext)
|
||||
user := authContext.User
|
||||
|
||||
if !user.IsAdmin {
|
||||
SendError(403, context, "permission denied")
|
||||
log.Info().Msg("failed to delete user: permission denied")
|
||||
return fmt.Errorf("permission denied")
|
||||
}
|
||||
|
||||
id := context.Param("id")
|
||||
|
||||
exists, error := r.userManager.UserIdExists(id)
|
||||
|
||||
if error != nil {
|
||||
SendError(500, context, fmt.Sprintf("Failed to delete user: %s", error))
|
||||
return error
|
||||
}
|
||||
|
||||
if !exists {
|
||||
SendError(404, context, fmt.Sprintf("Failed to delete user: Could not find user id %s", id))
|
||||
return fmt.Errorf("not found")
|
||||
}
|
||||
|
||||
error = r.userManager.DeleteUser(id)
|
||||
if error != nil {
|
||||
SendError(500, context, fmt.Sprintf("Failed to delete user: %s", error))
|
||||
return error
|
||||
}
|
||||
|
||||
return context.JSON(200, "")
|
||||
}
|
||||
|
||||
func (r *UsersApiHandler) removeTokenCookie(context *echo.Context) {
|
||||
cookie := &http.Cookie{
|
||||
Name: "token",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
MaxAge: -1,
|
||||
}
|
||||
|
||||
(*context).SetCookie(cookie)
|
||||
}
|
||||
|
||||
func (r *UsersApiHandler) handleUpdateConfig(context echo.Context) error {
|
||||
authContext := context.(AuthContext)
|
||||
user := authContext.User
|
||||
|
||||
if !user.IsAdmin {
|
||||
SendError(403, context, "permission denied")
|
||||
log.Info().Msg("failed to delete user: permission denied")
|
||||
return fmt.Errorf("permission denied")
|
||||
}
|
||||
|
||||
id := context.Param("id")
|
||||
|
||||
data := struct {
|
||||
IsAdmin int `json:"is_admin"`
|
||||
}{
|
||||
IsAdmin: -1,
|
||||
}
|
||||
|
||||
error := context.Bind(&data)
|
||||
|
||||
if error != nil {
|
||||
SendError(500, context, fmt.Sprintf("Failed to update user config: %s", error))
|
||||
return error
|
||||
}
|
||||
|
||||
updatingUser, error := r.userManager.GetUserById(id)
|
||||
|
||||
if error != nil {
|
||||
SendError(500, context, fmt.Sprintf("Failed to update user config: %s", error))
|
||||
return error
|
||||
}
|
||||
|
||||
if user.ID != id && data.IsAdmin != -1 {
|
||||
updatingUser.IsAdmin = data.IsAdmin == 1
|
||||
}
|
||||
|
||||
error = r.userManager.UpdateUser(updatingUser)
|
||||
|
||||
if error != nil {
|
||||
SendError(500, context, fmt.Sprintf("Failed to update user config: %s", error))
|
||||
return error
|
||||
}
|
||||
|
||||
return context.JSON(200, updatingUser)
|
||||
}
|
||||
|
||||
func (r *UsersApiHandler) SetRouter(router *echo.Echo) {
|
||||
r.router = router
|
||||
}
|
||||
|
||||
func (r *UsersApiHandler) SetUserManager(userManager *m.UserManager) {
|
||||
r.userManager = userManager
|
||||
}
|
||||
@ -69,7 +69,7 @@ func (r WebServer) Router() *echo.Echo {
|
||||
return r.router
|
||||
}
|
||||
|
||||
func sendError(statusCode int, context echo.Context, message string) {
|
||||
func SendError(statusCode int, context echo.Context, message string) {
|
||||
log.Info().Msg(message)
|
||||
responseData := struct {
|
||||
Error string `json:"error"`
|
||||
|
||||
Loading…
Reference in New Issue
Block a user