Files
hnh-map/internal/app/services/auth.go
Nikolay Tatarinov 5ffa10f8b7 Update project structure and enhance frontend functionality
- 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.
2026-02-25 16:32:55 +03:00

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)
})
}