From befaccd59cb252189b827d8638f7638961c2b9ed Mon Sep 17 00:00:00 2001 From: mstar Date: Mon, 7 Apr 2025 17:42:04 +0200 Subject: [PATCH] Move new web stuff into dedicated folder --- {webdebug => web/debug}/server.go | 4 + web/debug/users.go | 119 ++++++++++++++++++++++++++++++ {web-new => web/public}/server.go | 2 +- web/shared/User.go | 110 +++++++++++++++++++++++++++ 4 files changed, 234 insertions(+), 1 deletion(-) rename {webdebug => web/debug}/server.go (75%) create mode 100644 web/debug/users.go rename {web-new => web/public}/server.go (77%) create mode 100644 web/shared/User.go diff --git a/webdebug/server.go b/web/debug/server.go similarity index 75% rename from webdebug/server.go rename to web/debug/server.go index 14ebcca..9042b56 100644 --- a/webdebug/server.go +++ b/web/debug/server.go @@ -13,6 +13,9 @@ type Server struct { func New() *Server { handler := http.NewServeMux() + handler.HandleFunc("GET /non-deleted", getNonDeletedUsers) + handler.HandleFunc("POST /local-user", createLocalUser) + handler.HandleFunc("GET /delete", deleteUser) web := http.Server{ Addr: DebugAddr, Handler: handler, @@ -26,6 +29,7 @@ func (s *Server) Start() error { } return nil } + func (s *Server) Stop() error { return s.server.Shutdown(context.Background()) } diff --git a/web/debug/users.go b/web/debug/users.go new file mode 100644 index 0000000..37d0220 --- /dev/null +++ b/web/debug/users.go @@ -0,0 +1,119 @@ +package webdebug + +import ( + "crypto/ed25519" + "crypto/rand" + "database/sql" + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + httputils "git.mstar.dev/mstar/goutils/http" + "git.mstar.dev/mstar/goutils/sliceutils" + "github.com/rs/zerolog/log" + + "git.mstar.dev/mstar/linstrom/storage-new/dbgen" + "git.mstar.dev/mstar/linstrom/storage-new/models" + webshared "git.mstar.dev/mstar/linstrom/web/shared" +) + +func getNonDeletedUsers(w http.ResponseWriter, r *http.Request) { + pageStr := r.FormValue("page") + page := 0 + if pageStr != "" { + var err error + page, err = strconv.Atoi(pageStr) + if err != nil { + httputils.HttpErr(w, 0, "page is not a number", http.StatusBadRequest) + return + } + } + users, err := dbgen.User.GetPagedAllNonDeleted(uint(page)) + if err != nil { + httputils.HttpErr(w, 0, "failed to get users", http.StatusInternalServerError) + return + } + marshalled, err := json.Marshal(sliceutils.Map(users, func(t models.User) webshared.User { + u := webshared.User{} + u.FromModel(&t) + return u + })) + if err != nil { + httputils.HttpErr(w, 0, "failed to marshal users", http.StatusInternalServerError) + return + } + fmt.Fprint(w, string(marshalled)) +} + +func createLocalUser(w http.ResponseWriter, r *http.Request) { + type Inbound struct { + Username string `json:"username"` + Displayname string `json:"displayname"` + Description string `json:"description"` + Birthday *time.Time `json:"birthday"` + Location *string `json:"location"` + IsBot bool `json:"is_bot"` + } + jsonDecoder := json.NewDecoder(r.Body) + data := Inbound{} + err := jsonDecoder.Decode(&data) + if err != nil { + httputils.HttpErr(w, 0, "decode failed", http.StatusBadRequest) + return + } + + publicKey, privateKey, err := ed25519.GenerateKey(nil) + pkeyId := make([]byte, 64) + _, err = rand.Read(pkeyId) + if err != nil { + log.Error().Err(err).Msg("Failed to generate passkey id") + httputils.HttpErr(w, 0, "failed to generate passkey id", http.StatusInternalServerError) + return + } + + u := dbgen.User + query := u.Select( + u.Username, + u.DisplayName, + u.Description, + u.IsBot, + u.ServerId, + u.PrivateKey, + u.PublicKey, + u.PasskeyId, + ) + if data.Birthday != nil { + query = query.Select(u.Birthday) + } + if data.Location != nil { + query = query.Select(u.Location) + } + user := models.User{ + Username: data.Username, + DisplayName: data.Displayname, + Description: data.Description, + IsBot: data.IsBot, + ServerId: 1, // Hardcoded, Self is always first ID + PublicKey: publicKey, + PrivateKey: privateKey, + PasskeyId: pkeyId, + } + if data.Birthday != nil { + user.Birthday = sql.NullTime{Valid: true, Time: *data.Birthday} + } + if data.Location != nil { + user.Location = sql.NullString{Valid: true, String: *data.Location} + } + if err = u.Create(&user); err != nil { + log.Error().Err(err).Msg("failed to create new local user") + httputils.HttpErr(w, 0, "db failure", http.StatusInternalServerError) + } +} + +func deleteUser(w http.ResponseWriter, r *http.Request) { + id := r.FormValue("id") + dbgen.User.Where(dbgen.User.ID.Eq(id)).Delete() + w.WriteHeader(http.StatusOK) +} diff --git a/web-new/server.go b/web/public/server.go similarity index 77% rename from web-new/server.go rename to web/public/server.go index cbd9db5..df8c3a6 100644 --- a/web-new/server.go +++ b/web/public/server.go @@ -1,4 +1,4 @@ -package web +package webpublic import "net/http" diff --git a/web/shared/User.go b/web/shared/User.go new file mode 100644 index 0000000..5c11fc4 --- /dev/null +++ b/web/shared/User.go @@ -0,0 +1,110 @@ +package webshared + +import ( + "slices" + "time" + + "git.mstar.dev/mstar/linstrom/shared" + "git.mstar.dev/mstar/linstrom/storage-new/models" +) + +// Web/json representation of a user +type User struct { + // ---- Section public data ---- + // All data here will always be included, even if empty, + // in which case it will be marked as null instead of omitted + + ID string `json:"id"` + CreatedAt time.Time `json:"created_at"` + ServerId uint `json:"server_id"` + Displayname string `json:"displayname"` + Description string `json:"description"` + IsBot bool `json:"is_bot"` + IconId *string `json:"icon_id"` + BackgroundId *string `json:"background_id"` + BannerId *string `json:"banner_id"` + Indexable bool `json:"indexable"` + PublicKey []byte `json:"public_key"` + RestrictedFollow bool `json:"restricted_follow"` + Location *string `json:"location"` + Birthday *time.Time `json:"birthday"` + + // ---- Section Debug data ---- + // All these entries should only be available + // for the debug server and should be cleared + // before serving on the public server. + // Every debug field must be omitempty + Verified *bool `json:"verified,omitempty"` + FinishedRegistration *bool `json:"finished_registration,omitempty"` +} + +// Compiler assertations for interface implementations +var _ shared.Santisable = &User{} +var _ shared.Clonable = &User{} + +func (u *User) Sanitize() { + u.Verified = nil + u.FinishedRegistration = nil +} + +func (u *User) Clone() shared.Clonable { + user := *u + if u.IconId != nil { + tmp := *u.IconId + user.IconId = &tmp + } + if u.BackgroundId != nil { + tmp := *u.BackgroundId + user.BackgroundId = &tmp + } + if u.BannerId != nil { + tmp := *u.BannerId + user.BannerId = &tmp + } + if u.Location != nil { + tmp := *u.Location + user.Location = &tmp + } + if u.Birthday != nil { + tmp := *u.Birthday + user.Birthday = &tmp + } + if u.Verified != nil { + tmp := *u.Verified + user.Verified = &tmp + } + if u.FinishedRegistration != nil { + tmp := *u.FinishedRegistration + user.FinishedRegistration = &tmp + } + user.PublicKey = slices.Clone(u.PublicKey) + return &user +} + +func (u *User) FromModel(m *models.User) { + u.ID = m.ID + u.CreatedAt = m.CreatedAt + u.ServerId = m.ServerId + u.Displayname = m.DisplayName + u.IsBot = m.IsBot + if m.IconId.Valid { + u.IconId = &m.IconId.String + } + if m.BackgroundId.Valid { + u.BackgroundId = &m.IconId.String + } + if m.BannerId.Valid { + u.BannerId = &m.IconId.String + } + u.Indexable = m.Indexable + u.PublicKey = append(u.PublicKey, m.PublicKey...) + u.RestrictedFollow = m.RestrictedFollow + if m.Location.Valid { + u.Location = &m.Location.String + } + if m.Birthday.Valid { + u.Birthday = &m.Birthday.Time + } + u.Verified = &m.Verified + u.FinishedRegistration = &m.FinishedRegistration +}