Add initial project structure with backend and frontend setup
- Created backend structure with Go, including main application logic and API endpoints. - Added Docker support for both development and production environments. - Introduced frontend using Nuxt 3 with Tailwind CSS for styling. - Included configuration files for Docker and environment variables. - Established basic documentation for contributing, development, and deployment processes. - Set up .gitignore and .dockerignore files to manage ignored files in the repository.
This commit is contained in:
331
internal/app/app.go
Normal file
331
internal/app/app.go
Normal file
@@ -0,0 +1,331 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/andyleap/hnh-map/webapp"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// App is the main application (map server) state.
|
||||
type App struct {
|
||||
gridStorage string
|
||||
frontendRoot string
|
||||
db *bbolt.DB
|
||||
|
||||
characters map[string]Character
|
||||
chmu sync.RWMutex
|
||||
|
||||
*webapp.WebApp
|
||||
|
||||
gridUpdates topic
|
||||
mergeUpdates mergeTopic
|
||||
}
|
||||
|
||||
// NewApp creates an App with the given storage paths and database.
|
||||
// frontendRoot is the directory for the map SPA (e.g. "frontend").
|
||||
func NewApp(gridStorage, frontendRoot string, db *bbolt.DB) (*App, error) {
|
||||
w, err := webapp.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &App{
|
||||
gridStorage: gridStorage,
|
||||
frontendRoot: frontendRoot,
|
||||
db: db,
|
||||
characters: make(map[string]Character),
|
||||
WebApp: w,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
ID string
|
||||
Username string
|
||||
Auths Auths `json:"-"`
|
||||
TempAdmin bool
|
||||
}
|
||||
|
||||
type Character struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
Map int `json:"map"`
|
||||
Position Position `json:"position"`
|
||||
Type string `json:"type"`
|
||||
updated time.Time
|
||||
}
|
||||
|
||||
type Marker struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
GridID string `json:"gridID"`
|
||||
Position Position `json:"position"`
|
||||
Image string `json:"image"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
type FrontendMarker struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
Map int `json:"map"`
|
||||
Position Position `json:"position"`
|
||||
Image string `json:"image"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
type MapInfo struct {
|
||||
ID int
|
||||
Name string
|
||||
Hidden bool
|
||||
Priority bool
|
||||
}
|
||||
|
||||
type GridData struct {
|
||||
ID string
|
||||
Coord Coord
|
||||
NextUpdate time.Time
|
||||
Map int
|
||||
}
|
||||
|
||||
type Coord struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
func (c Coord) Name() string {
|
||||
return fmt.Sprintf("%d_%d", c.X, c.Y)
|
||||
}
|
||||
|
||||
func (c Coord) Parent() Coord {
|
||||
if c.X < 0 {
|
||||
c.X--
|
||||
}
|
||||
if c.Y < 0 {
|
||||
c.Y--
|
||||
}
|
||||
return Coord{
|
||||
X: c.X / 2,
|
||||
Y: c.Y / 2,
|
||||
}
|
||||
}
|
||||
|
||||
type Auths []string
|
||||
|
||||
func (a Auths) Has(auth string) bool {
|
||||
for _, v := range a {
|
||||
if v == auth {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const (
|
||||
AUTH_ADMIN = "admin"
|
||||
AUTH_MAP = "map"
|
||||
AUTH_MARKERS = "markers"
|
||||
AUTH_UPLOAD = "upload"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Pass []byte
|
||||
Auths Auths
|
||||
Tokens []string
|
||||
}
|
||||
|
||||
func (a *App) getSession(req *http.Request) *Session {
|
||||
c, err := req.Cookie("session")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var s *Session
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
sessions := tx.Bucket([]byte("sessions"))
|
||||
if sessions == nil {
|
||||
return nil
|
||||
}
|
||||
session := sessions.Get([]byte(c.Value))
|
||||
if session == nil {
|
||||
return nil
|
||||
}
|
||||
err := json.Unmarshal(session, &s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.TempAdmin {
|
||||
s.Auths = Auths{AUTH_ADMIN}
|
||||
return nil
|
||||
}
|
||||
users := tx.Bucket([]byte("users"))
|
||||
if users == nil {
|
||||
return nil
|
||||
}
|
||||
raw := users.Get([]byte(s.Username))
|
||||
if raw == nil {
|
||||
s = nil
|
||||
return nil
|
||||
}
|
||||
u := User{}
|
||||
err = json.Unmarshal(raw, &u)
|
||||
if err != nil {
|
||||
s = nil
|
||||
return err
|
||||
}
|
||||
s.Auths = u.Auths
|
||||
return nil
|
||||
})
|
||||
return s
|
||||
}
|
||||
|
||||
func (a *App) deleteSession(s *Session) {
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
sessions, err := tx.CreateBucketIfNotExists([]byte("sessions"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sessions.Delete([]byte(s.ID))
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) saveSession(s *Session) {
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
sessions, err := tx.CreateBucketIfNotExists([]byte("sessions"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sessions.Put([]byte(s.ID), buf)
|
||||
})
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
func (a *App) getPage(req *http.Request) Page {
|
||||
p := Page{}
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
c := tx.Bucket([]byte("config"))
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
p.Title = string(c.Get([]byte("title")))
|
||||
return nil
|
||||
})
|
||||
return p
|
||||
}
|
||||
|
||||
func (a *App) getUser(user, pass string) (u *User) {
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
users := tx.Bucket([]byte("users"))
|
||||
if users == nil {
|
||||
return nil
|
||||
}
|
||||
raw := users.Get([]byte(user))
|
||||
if raw != nil {
|
||||
json.Unmarshal(raw, &u)
|
||||
if bcrypt.CompareHashAndPassword(u.Pass, []byte(pass)) != nil {
|
||||
u = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return u
|
||||
}
|
||||
|
||||
// serveMapFrontend serves the map SPA: static files from frontend, fallback to index.html for client-side routes.
|
||||
func (a *App) serveMapFrontend(rw http.ResponseWriter, req *http.Request) {
|
||||
path := req.URL.Path
|
||||
if len(path) <= len("/map/") {
|
||||
path = ""
|
||||
} else {
|
||||
path = path[len("/map/"):]
|
||||
}
|
||||
root := a.frontendRoot
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
}
|
||||
path = filepath.Clean(path)
|
||||
if path == "." || path == ".." || (len(path) >= 2 && path[:2] == "..") {
|
||||
http.NotFound(rw, req)
|
||||
return
|
||||
}
|
||||
tryPaths := []string{filepath.Join("map", path), path}
|
||||
var f http.File
|
||||
for _, p := range tryPaths {
|
||||
var err error
|
||||
f, err = http.Dir(root).Open(p)
|
||||
if err == nil {
|
||||
path = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if f == nil {
|
||||
http.ServeFile(rw, req, filepath.Join(root, "index.html"))
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
stat, err := f.Stat()
|
||||
if err != nil || stat.IsDir() {
|
||||
http.ServeFile(rw, req, filepath.Join(root, "index.html"))
|
||||
return
|
||||
}
|
||||
http.ServeContent(rw, req, stat.Name(), stat.ModTime(), f)
|
||||
}
|
||||
|
||||
// CleanChars runs a background loop that removes stale character entries. Call once as a goroutine.
|
||||
func (a *App) CleanChars() {
|
||||
for range time.Tick(time.Second * 10) {
|
||||
a.chmu.Lock()
|
||||
for n, c := range a.characters {
|
||||
if c.updated.Before(time.Now().Add(-10 * time.Second)) {
|
||||
delete(a.characters, n)
|
||||
}
|
||||
}
|
||||
a.chmu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers all HTTP handlers for the app.
|
||||
func (a *App) RegisterRoutes() {
|
||||
http.HandleFunc("/client/", a.client)
|
||||
|
||||
http.HandleFunc("/login", a.redirectLogin)
|
||||
http.HandleFunc("/logout", a.logout)
|
||||
http.HandleFunc("/", a.redirectRoot)
|
||||
http.HandleFunc("/generateToken", a.generateToken)
|
||||
http.HandleFunc("/password", a.changePassword)
|
||||
|
||||
http.HandleFunc("/admin/", a.admin)
|
||||
http.HandleFunc("/admin/user", a.adminUser)
|
||||
http.HandleFunc("/admin/deleteUser", a.deleteUser)
|
||||
http.HandleFunc("/admin/wipe", a.wipe)
|
||||
http.HandleFunc("/admin/setPrefix", a.setPrefix)
|
||||
http.HandleFunc("/admin/setDefaultHide", a.setDefaultHide)
|
||||
http.HandleFunc("/admin/setTitle", a.setTitle)
|
||||
http.HandleFunc("/admin/rebuildZooms", a.rebuildZooms)
|
||||
http.HandleFunc("/admin/export", a.export)
|
||||
http.HandleFunc("/admin/merge", a.merge)
|
||||
http.HandleFunc("/admin/map", a.adminMap)
|
||||
http.HandleFunc("/admin/mapic", a.adminICMap)
|
||||
|
||||
http.HandleFunc("/map/api/", a.apiRouter)
|
||||
http.HandleFunc("/map/updates", a.watchGridUpdates)
|
||||
http.HandleFunc("/map/grids/", a.gridTile)
|
||||
http.HandleFunc("/map/", a.serveMapFrontend)
|
||||
}
|
||||
Reference in New Issue
Block a user