From df3fe83c5f7a48a5d06cac1e693875afae31dd34 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Tue, 11 Jun 2024 13:42:58 -0500 Subject: [PATCH] Finished with crash requests Change Log to Count Added option to get user count Moved functions to VerifyHeader Added user delete --- internal/darkstorm_backend/README.md | 76 +++++++++++++---- internal/darkstorm_backend/app.go | 2 +- internal/darkstorm_backend/crash.go | 107 +++++++++++++++++------- internal/darkstorm_backend/darkstorm.go | 11 ++- internal/darkstorm_backend/db.go | 9 +- internal/darkstorm_backend/header.go | 2 +- internal/darkstorm_backend/log.go | 41 +++++++-- internal/darkstorm_backend/user.go | 77 +++++++++++------ 8 files changed, 241 insertions(+), 84 deletions(-) diff --git a/internal/darkstorm_backend/README.md b/internal/darkstorm_backend/README.md index 90ad894..416026c 100644 --- a/internal/darkstorm_backend/README.md +++ b/internal/darkstorm_backend/README.md @@ -6,8 +6,6 @@ This is a purposefully "simple" application backend made specifically for _my_ a ### API Key -The special appID "darkstormManagement" is used to manage all apps. - ```json { id: "API Key", @@ -15,7 +13,7 @@ The special appID "darkstormManagement" is used to manage all apps. death: -1, // unix timestamp (seconds) when the key is no longer valid. -1 means there is not expected expiration (that can change in the future) perm: { user: true, // create and login users - log: true, // log users + count: true, // count users crash: true, // crash reports management: false, // managing // further permissions can be added as needed @@ -23,7 +21,9 @@ The special appID "darkstormManagement" is used to manage all apps. } ``` -### DB Log +Optionally you can set a special AppID to be a management key. Setting a management key enables management requests. + +### Count log ```json { @@ -111,16 +111,42 @@ If an error status code is returned then the body will be as follows. * API Key is invalid or does not have the needed permission for the request. * invalidBody * Body of the request is malformed. +* badRequest + * Some part of your request is invalid * internal * Server-side issue. -### Log +### Count -API Key must have the `log` permission. +API Key must have the `Count` permission. Request: -> POST: /log/{uuid} +> POST: /count/{uuid} + +### User Count + +Get a count of users. + +API Key must have the `management` permission. + +`platform` query is optional (defaults to all). + +Request: + +> GET: /count?platform=all + +With management key: + +> GET: /{appID}/count?platform=all + +Returns: + +```json +{ + count: 0 +} +``` ### Users @@ -128,6 +154,8 @@ Request: All requsests pertaining to users requires the `X-API-Key` header and the key must have the `users` permission. +Enabled by using `Backend.AddUserAuth`. + #### Create User > TODO: Email user to confirm. @@ -164,11 +192,19 @@ If returned status is 401, the errorCode will be one of the following: * password * Password is to short or too long. +#### Delete User + +Requires either the `management` permission or a management key. + +Request: + +> DELETE: /user/{userID} + #### Login Request: -> POST: /user +> POST: /user/login ```json { @@ -196,9 +232,21 @@ Possible `error` values: * invalid * Either the username or password is incorrect -### Crash Report +#### Change Password -> TODO: Archive a crash to prevent it being reported again. +Request: + +> POST: /user/changepassword + +```json +{ + token: "JWT Token", + old: "Old Password", + new: "New Password" +} +``` + +### Crash Report #### Report @@ -227,7 +275,7 @@ Request: > DELETE: /crash/{crashID} -With "darkstormManagement" key: +With management key: > DELETE: /{appID}/crash/{crashID} @@ -239,16 +287,16 @@ Request: > POST: /crash/archive -With "darkstormManagement" key: +With management key: -> POST: /{appID}/crash/{crashID} +> POST: /{appID}/crash/archive Request Body: ```json { error: "error", - stack: "full stacktrace", + stack: "full stacktrace", // Archives will only match against a perfect match. platform: "all", // Limit the archive to a specific platform, or use "all". } ``` diff --git a/internal/darkstorm_backend/app.go b/internal/darkstorm_backend/app.go index 8a9545a..6b3d812 100644 --- a/internal/darkstorm_backend/app.go +++ b/internal/darkstorm_backend/app.go @@ -5,7 +5,7 @@ import "net/http" // An application interface. Both LogTable and CrashTable are optional, if they return nil then requests will be forbidden. type App interface { AppID() string - LogTable() LogTable + CountTable() CountTable CrashTable() CrashTable } diff --git a/internal/darkstorm_backend/crash.go b/internal/darkstorm_backend/crash.go index 21f75e7..6748a27 100644 --- a/internal/darkstorm_backend/crash.go +++ b/internal/darkstorm_backend/crash.go @@ -2,7 +2,6 @@ package darkstorm import ( "encoding/json" - "errors" "log" "net/http" ) @@ -65,55 +64,105 @@ func (b *Backend) reportCrash(w http.ResponseWriter, r *http.Request) { } func (b *Backend) deleteCrash(w http.ResponseWriter, r *http.Request) { - hdr, err := b.ParseHeader(r) - if hdr.Key == nil || hdr.Key.Perm["management"] || errors.Is(err, ErrApiKeyUnauthorized) { - ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") - return - } else if err != nil { - ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + hdr, err := b.VerifyHeader(w, r, "management", false) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } return } - //TODO + crashID := r.PathValue("crashID") + if crashID == "" { + ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request") + return + } + b.actualCrashDelete(w, b.GetApp(hdr.Key), crashID) } func (b *Backend) managementDeleteCrash(w http.ResponseWriter, r *http.Request) { - hdr, err := b.ParseHeader(r) - if hdr.Key == nil || hdr.Key.Perm["management"] || errors.Is(err, ErrApiKeyUnauthorized) { - ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") - return - } else if err != nil { - ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + hdr, err := b.VerifyHeader(w, r, "management", true) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } return } - //TODO + crashID := r.PathValue("crashID") + if crashID == "" { + ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request") + return + } + appID := r.PathValue("appID") + ap := b.apps[appID] + if ap == nil || appID == "" { + ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request") + return + } + b.actualCrashDelete(w, ap, crashID) } -func (b *Backend) actualCrashDelete(w http.ResponseWriter, ap App, crashID string) {} +func (b *Backend) actualCrashDelete(w http.ResponseWriter, ap App, crashID string) { + crash := ap.CrashTable() + if crash == nil { + ReturnError(w, http.StatusInternalServerError, "misconfigured", "Server Misconfigured") + return + } + err := crash.Remove(crashID) + if err != nil && err != ErrNotFound { + log.Println("error when deleting crash:", err) + } +} func (b *Backend) archiveCrash(w http.ResponseWriter, r *http.Request) { - hdr, err := b.ParseHeader(r) - if hdr.Key == nil || hdr.Key.Perm["management"] { - w.WriteHeader(http.StatusUnauthorized) + hdr, err := b.VerifyHeader(w, r, "management", false) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } return } - if err != nil { - //TODO + defer r.Body.Close() + var toArchive ArchivedCrash + err = json.NewDecoder(r.Body).Decode(&toArchive) + if err != nil || toArchive.Platform == "" || toArchive.Error == "" || toArchive.Stack == "" { + ReturnError(w, http.StatusBadRequest, "invalidBody", "Bad request") return } - //TODO + b.actualCrashArchive(w, b.GetApp(hdr.Key), toArchive) } func (b *Backend) managementArchiveCrash(w http.ResponseWriter, r *http.Request) { - hdr, err := b.ParseHeader(r) - if hdr.Key == nil || hdr.Key.Perm["management"] { - w.WriteHeader(http.StatusUnauthorized) + hdr, err := b.VerifyHeader(w, r, "management", true) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } return } - if err != nil { - //TODO + appID := r.PathValue("appID") + ap := b.apps[appID] + if ap == nil || appID == "" { + ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request") return } - //TODO + defer r.Body.Close() + var toArchive ArchivedCrash + err = json.NewDecoder(r.Body).Decode(&toArchive) + if err != nil || toArchive.Platform == "" || toArchive.Error == "" || toArchive.Stack == "" { + ReturnError(w, http.StatusBadRequest, "invalidBody", "Bad request") + return + } + b.actualCrashArchive(w, ap, toArchive) } -func (b *Backend) actualCrashArchive(w http.ResponseWriter, ap App, toArchive ArchivedCrash) {} +func (b *Backend) actualCrashArchive(w http.ResponseWriter, ap App, toArchive ArchivedCrash) { + crash := ap.CrashTable() + if crash == nil { + ReturnError(w, http.StatusInternalServerError, "misconfigured", "Server Misconfigured") + return + } + err := crash.Archive(toArchive) + if err != nil { + log.Println() + } +} diff --git a/internal/darkstorm_backend/darkstorm.go b/internal/darkstorm_backend/darkstorm.go index 76b0593..8feee4b 100644 --- a/internal/darkstorm_backend/darkstorm.go +++ b/internal/darkstorm_backend/darkstorm.go @@ -37,7 +37,7 @@ func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) { if ext, is := apps[i].(ExtendedApp); is { b.m.HandleFunc("/"+apps[i].AppID()+"/", ext.Extension) } - if !hasLog && apps[i].LogTable() != nil { + if !hasLog && apps[i].CountTable() != nil { hasLog = true } if !hasCrash && apps[i].CrashTable() != nil { @@ -45,7 +45,8 @@ func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) { } } if hasLog { - b.m.HandleFunc("POST /log/{uuid}", b.log) + b.m.HandleFunc("POST /count/{uuid}", b.countLog) + b.m.HandleFunc("GET /count", b.getCount) } if hasCrash { b.m.HandleFunc("POST /crash", b.reportCrash) @@ -61,7 +62,7 @@ func (b *Backend) cleanupLoop() { oldTim := time.Now().Add(-30 * 24 * time.Hour) old := (oldTim.Year() * 10000) + (int(oldTim.Month()) * 100) + oldTim.Day() for _, a := range b.apps { - tab := a.LogTable() + tab := a.CountTable() if tab == nil { continue } @@ -74,12 +75,16 @@ func (b *Backend) EnableManagementKey(managementID string) { b.managementKeyID = managementID b.m.HandleFunc("DELETE /{appID}/crash/{crashID}", b.managementDeleteCrash) b.m.HandleFunc("POST /{appID}/crash/archive", b.managementArchiveCrash) + b.m.HandleFunc("GET /{appID}/count", b.getCount) } func (b *Backend) AddUserAuth(userTable Table[User], privKey, pubKey []byte) { b.userTable = userTable b.jwtPriv = privKey b.jwtPub = pubKey + b.m.HandleFunc("POST /user/create", b.createUser) + b.m.HandleFunc("DELETE /user/{userID}", b.deleteUser) + b.m.HandleFunc("POST /user/login", b.login) } func (b *Backend) HandleFunc(pattern string, h http.HandlerFunc) { diff --git a/internal/darkstorm_backend/db.go b/internal/darkstorm_backend/db.go index fd69475..f58b8a1 100644 --- a/internal/darkstorm_backend/db.go +++ b/internal/darkstorm_backend/db.go @@ -19,17 +19,18 @@ type Table[T IDStruct] interface { PartUpdate(ID string, update map[string]any) error } -type LogTable interface { - Table[Log] - // Remove all Log items that have a Log.Date value less then the given value. +type CountTable interface { + Table[CountLog] + // Remove all Log items that have a CountLog.Date value less then the given value. RemoveOldLogs(date int) + Count(platform string) int } type CrashTable interface { Table[CrashReport] // Move a crash type to archive. All instances that perfectly match that appear in CrashReport.Individual should be deleted. // If a CrashReport ends up with an empty Individual array it should also be deleted. - Archive(ArchivedCrash) + Archive(ArchivedCrash) error IsArchived(IndividualCrash) bool // Add the IndividualCrash report to the crash table. If a CrashReport exists that matches, then it gets added to CrashReport.Individual. // If an IndividualCrash exists that is a perfect match, Count is incremented instead of adding it to the array. diff --git a/internal/darkstorm_backend/header.go b/internal/darkstorm_backend/header.go index ead4d78..cfcbe0c 100644 --- a/internal/darkstorm_backend/header.go +++ b/internal/darkstorm_backend/header.go @@ -15,7 +15,7 @@ var ( ) type ParsedHeader struct { - User *ReqUser + User *ReqestUser Key *ApiKey } diff --git a/internal/darkstorm_backend/log.go b/internal/darkstorm_backend/log.go index 309fb48..bf4cb0a 100644 --- a/internal/darkstorm_backend/log.go +++ b/internal/darkstorm_backend/log.go @@ -1,17 +1,48 @@ package darkstorm -import "net/http" +import ( + "log" + "net/http" +) -type Log struct { +type CountLog struct { ID string Platform string Date int } -func (l Log) GetID() string { - return l.ID +func (c CountLog) GetID() string { + return c.ID } -func (b *Backend) log(w http.ResponseWriter, r *http.Request) { +func (b *Backend) countLog(w http.ResponseWriter, r *http.Request) { + hdr, err := b.VerifyHeader(w, r, "count", false) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } + return + } + //TODO +} + +func (b *Backend) getCount(w http.ResponseWriter, r *http.Request) { + hdr, err := b.VerifyHeader(w, r, "management", true) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } + return + } + var ap App + if hdr.Key.AppID == b.managementKeyID { + ap = b.apps[r.PathValue("appID")] + if ap == nil { + ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request") + return + } + } else { + ap = b.GetApp(hdr.Key) + } //TODO } diff --git a/internal/darkstorm_backend/user.go b/internal/darkstorm_backend/user.go index e28e0ef..4555326 100644 --- a/internal/darkstorm_backend/user.go +++ b/internal/darkstorm_backend/user.go @@ -24,6 +24,24 @@ func generateSalt() (string, error) { return base64.RawStdEncoding.EncodeToString(out), err } +type ReqestUser struct { + Perm map[string]string + ID string + Username string +} + +func (b *Backend) GenerateJWT(r *ReqestUser) (string, error) { + if b.jwtPriv == nil || b.jwtPub == nil { + return "", errors.New("user management not enabled") + } + return jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.RegisteredClaims{ + ID: r.ID, + Issuer: "darkstorm.tech", + IssuedAt: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(12 * time.Hour)), + }).SignedString(b.jwtPriv) +} + type User struct { Perm map[string]string `json:"perm" bson:"perm"` ID string `json:"id" bson:"_id"` @@ -59,27 +77,12 @@ func NewUser(username, password, email string) (User, error) { return u, nil } -type ReqUser struct { - Perm map[string]string - ID string - Username string -} - -func (b *Backend) generateJWT(r *ReqUser) (string, error) { - return jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.RegisteredClaims{ - ID: r.ID, - Issuer: "darkstorm.tech", - IssuedAt: jwt.NewNumericDate(time.Now()), - ExpiresAt: jwt.NewNumericDate(time.Now().Add(12 * time.Hour)), - }).SignedString(b.jwtPriv) -} - func (u User) GetID() string { return u.ID } -func (u User) toReqUser() *ReqUser { - return &ReqUser{ +func (u User) toReqUser() *ReqestUser { + return &ReqestUser{ Perm: u.Perm, ID: u.ID, Username: u.Username, @@ -114,7 +117,7 @@ type createUserReturn struct { Token string `json:"token"` } -func (b *Backend) CreateUser(w http.ResponseWriter, r *http.Request) { +func (b *Backend) createUser(w http.ResponseWriter, r *http.Request) { hdr, err := b.VerifyHeader(w, r, "user", false) if hdr == nil { if err == nil { @@ -164,7 +167,7 @@ func (b *Backend) CreateUser(w http.ResponseWriter, r *http.Request) { } var ret createUserReturn ret.Username = u.Username - ret.Token, err = b.generateJWT(u.toReqUser()) + ret.Token, err = b.GenerateJWT(u.toReqUser()) if err != nil { ReturnError(w, http.StatusInternalServerError, "internal", "Server error") return @@ -173,6 +176,27 @@ func (b *Backend) CreateUser(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(ret) } +func (b *Backend) deleteUser(w http.ResponseWriter, r *http.Request) { + hdr, err := b.VerifyHeader(w, r, "management", true) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } + return + } + userID := r.PathValue("userID") + if userID == "" { + ReturnError(w, http.StatusBadRequest, "badRequest", "Bad Request") + return + } + b.userMutex.Lock() + defer b.userMutex.Unlock() + err = b.userTable.Remove(userID) + if err != nil && err != ErrNotFound { + log.Println("error deleting user:", err) + } +} + type loginRequest struct { Username string Password string @@ -184,13 +208,12 @@ type loginReturn struct { Timeout int64 `json:"timeout"` } -func (b *Backend) Login(w http.ResponseWriter, r *http.Request) { - hdr, err := b.ParseHeader(r) - if hdr.Key == nil || !hdr.Key.Perm["user"] || errors.Is(err, ErrApiKeyUnauthorized) { - ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") - return - } else if err != nil { - ReturnError(w, http.StatusInternalServerError, "internal", "Server error") +func (b *Backend) login(w http.ResponseWriter, r *http.Request) { + hdr, err := b.VerifyHeader(w, r, "user", false) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } return } defer r.Body.Close() @@ -224,7 +247,7 @@ func (b *Backend) Login(w http.ResponseWriter, r *http.Request) { return } if u.Password == hash { - ret.Token, err = b.generateJWT(u.toReqUser()) + ret.Token, err = b.GenerateJWT(u.toReqUser()) if err != nil { ReturnError(w, http.StatusInternalServerError, "internal", "Server error") return