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