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.
This commit is contained in:
242
internal/app/services/auth.go
Normal file
242
internal/app/services/auth.go
Normal file
@@ -0,0 +1,242 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user