- Added a new AGENTS.md file to document the project structure and conventions. - Updated .gitignore to include node_modules and refined cursor rules. - Introduced new backend and frontend components for improved map interactions, including context menus and controls. - Enhanced API composables for better admin and authentication functionalities. - Refactored existing components for cleaner code and improved user experience. - Updated README.md to clarify production asset serving and user setup instructions.
243 lines
5.9 KiB
Go
243 lines
5.9 KiB
Go
package services
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/andyleap/hnh-map/internal/app"
|
|
"github.com/andyleap/hnh-map/internal/app/store"
|
|
"go.etcd.io/bbolt"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// AuthService handles authentication and session business logic.
|
|
type AuthService struct {
|
|
st *store.Store
|
|
}
|
|
|
|
// NewAuthService creates an AuthService.
|
|
func NewAuthService(st *store.Store) *AuthService {
|
|
return &AuthService{st: st}
|
|
}
|
|
|
|
// GetSession returns the session from the request cookie, or nil.
|
|
func (s *AuthService) GetSession(req *http.Request) *app.Session {
|
|
c, err := req.Cookie("session")
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var sess *app.Session
|
|
s.st.View(func(tx *bbolt.Tx) error {
|
|
raw := s.st.GetSession(tx, c.Value)
|
|
if raw == nil {
|
|
return nil
|
|
}
|
|
if err := json.Unmarshal(raw, &sess); err != nil {
|
|
return err
|
|
}
|
|
if sess.TempAdmin {
|
|
sess.Auths = app.Auths{app.AUTH_ADMIN}
|
|
return nil
|
|
}
|
|
uRaw := s.st.GetUser(tx, sess.Username)
|
|
if uRaw == nil {
|
|
sess = nil
|
|
return nil
|
|
}
|
|
var u app.User
|
|
if err := json.Unmarshal(uRaw, &u); err != nil {
|
|
sess = nil
|
|
return err
|
|
}
|
|
sess.Auths = u.Auths
|
|
return nil
|
|
})
|
|
return sess
|
|
}
|
|
|
|
// DeleteSession removes a session.
|
|
func (s *AuthService) DeleteSession(sess *app.Session) {
|
|
s.st.Update(func(tx *bbolt.Tx) error {
|
|
return s.st.DeleteSession(tx, sess.ID)
|
|
})
|
|
}
|
|
|
|
// SaveSession stores a session.
|
|
func (s *AuthService) SaveSession(sess *app.Session) error {
|
|
return s.st.Update(func(tx *bbolt.Tx) error {
|
|
buf, err := json.Marshal(sess)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.st.PutSession(tx, sess.ID, buf)
|
|
})
|
|
}
|
|
|
|
// CreateSession creates a session for username, returns session ID or empty string.
|
|
func (s *AuthService) CreateSession(username string, tempAdmin bool) string {
|
|
session := make([]byte, 32)
|
|
if _, err := rand.Read(session); err != nil {
|
|
return ""
|
|
}
|
|
sid := hex.EncodeToString(session)
|
|
sess := &app.Session{
|
|
ID: sid,
|
|
Username: username,
|
|
TempAdmin: tempAdmin,
|
|
}
|
|
if s.SaveSession(sess) != nil {
|
|
return ""
|
|
}
|
|
return sid
|
|
}
|
|
|
|
// GetUser returns user if username/password match.
|
|
func (s *AuthService) GetUser(username, pass string) *app.User {
|
|
var u *app.User
|
|
s.st.View(func(tx *bbolt.Tx) error {
|
|
raw := s.st.GetUser(tx, username)
|
|
if raw == nil {
|
|
return nil
|
|
}
|
|
json.Unmarshal(raw, &u)
|
|
if u.Pass == nil {
|
|
u = nil
|
|
return nil
|
|
}
|
|
if bcrypt.CompareHashAndPassword(u.Pass, []byte(pass)) != nil {
|
|
u = nil
|
|
return nil
|
|
}
|
|
return nil
|
|
})
|
|
return u
|
|
}
|
|
|
|
// GetUserByUsername returns user without password check (for OAuth-only check).
|
|
func (s *AuthService) GetUserByUsername(username string) *app.User {
|
|
var u *app.User
|
|
s.st.View(func(tx *bbolt.Tx) error {
|
|
raw := s.st.GetUser(tx, username)
|
|
if raw != nil {
|
|
json.Unmarshal(raw, &u)
|
|
}
|
|
return nil
|
|
})
|
|
return u
|
|
}
|
|
|
|
// SetupRequired returns true if no users exist (first run).
|
|
func (s *AuthService) SetupRequired() bool {
|
|
var required bool
|
|
s.st.View(func(tx *bbolt.Tx) error {
|
|
if s.st.UserCount(tx) == 0 {
|
|
required = true
|
|
}
|
|
return nil
|
|
})
|
|
return required
|
|
}
|
|
|
|
// BootstrapAdmin creates the first admin user if bootstrap env is set and no users exist.
|
|
// Returns the user if created, nil otherwise.
|
|
func (s *AuthService) BootstrapAdmin(username, pass, bootstrapEnv string) *app.User {
|
|
if username != "admin" || pass == "" || bootstrapEnv == "" || pass != bootstrapEnv {
|
|
return nil
|
|
}
|
|
var created bool
|
|
var u *app.User
|
|
s.st.Update(func(tx *bbolt.Tx) error {
|
|
if s.st.GetUser(tx, "admin") != nil {
|
|
return nil
|
|
}
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
user := app.User{
|
|
Pass: hash,
|
|
Auths: app.Auths{app.AUTH_ADMIN, app.AUTH_MAP, app.AUTH_MARKERS, app.AUTH_UPLOAD},
|
|
}
|
|
raw, _ := json.Marshal(user)
|
|
if err := s.st.PutUser(tx, "admin", raw); err != nil {
|
|
return err
|
|
}
|
|
created = true
|
|
u = &user
|
|
return nil
|
|
})
|
|
if created {
|
|
return u
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetBootstrapPassword returns HNHMAP_BOOTSTRAP_PASSWORD from env.
|
|
func GetBootstrapPassword() string {
|
|
return os.Getenv("HNHMAP_BOOTSTRAP_PASSWORD")
|
|
}
|
|
|
|
// GetUserTokensAndPrefix returns tokens and config prefix for a user.
|
|
func (s *AuthService) GetUserTokensAndPrefix(username string) (tokens []string, prefix string) {
|
|
s.st.View(func(tx *bbolt.Tx) error {
|
|
uRaw := s.st.GetUser(tx, username)
|
|
if uRaw != nil {
|
|
var u app.User
|
|
json.Unmarshal(uRaw, &u)
|
|
tokens = u.Tokens
|
|
}
|
|
if p := s.st.GetConfig(tx, "prefix"); p != nil {
|
|
prefix = string(p)
|
|
}
|
|
return nil
|
|
})
|
|
return tokens, prefix
|
|
}
|
|
|
|
// GenerateTokenForUser adds a new token for user and returns the full list.
|
|
func (s *AuthService) GenerateTokenForUser(username string) []string {
|
|
tokenRaw := make([]byte, 16)
|
|
if _, err := rand.Read(tokenRaw); err != nil {
|
|
return nil
|
|
}
|
|
token := hex.EncodeToString(tokenRaw)
|
|
var tokens []string
|
|
s.st.Update(func(tx *bbolt.Tx) error {
|
|
uRaw := s.st.GetUser(tx, username)
|
|
u := app.User{}
|
|
if uRaw != nil {
|
|
json.Unmarshal(uRaw, &u)
|
|
}
|
|
u.Tokens = append(u.Tokens, token)
|
|
tokens = u.Tokens
|
|
buf, _ := json.Marshal(u)
|
|
s.st.PutUser(tx, username, buf)
|
|
return s.st.PutToken(tx, token, username)
|
|
})
|
|
return tokens
|
|
}
|
|
|
|
// SetUserPassword sets password for user (empty pass = no change).
|
|
func (s *AuthService) SetUserPassword(username, pass string) error {
|
|
if pass == "" {
|
|
return nil
|
|
}
|
|
return s.st.Update(func(tx *bbolt.Tx) error {
|
|
uRaw := s.st.GetUser(tx, username)
|
|
u := app.User{}
|
|
if uRaw != nil {
|
|
json.Unmarshal(uRaw, &u)
|
|
}
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
u.Pass = hash
|
|
raw, _ := json.Marshal(u)
|
|
return s.st.PutUser(tx, username, raw)
|
|
})
|
|
}
|