Finished with crash requests

Change Log to Count
Added option to get user count
Moved functions to VerifyHeader
Added user delete
This commit is contained in:
Caleb Gardner
2024-06-11 13:42:58 -05:00
parent 99c881b51e
commit df3fe83c5f
8 changed files with 241 additions and 84 deletions
+62 -14
View File
@@ -6,8 +6,6 @@ This is a purposefully "simple" application backend made specifically for _my_ a
### API Key ### API Key
The special appID "darkstormManagement" is used to manage all apps.
```json ```json
{ {
id: "API Key", 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) 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: { perm: {
user: true, // create and login users user: true, // create and login users
log: true, // log users count: true, // count users
crash: true, // crash reports crash: true, // crash reports
management: false, // managing management: false, // managing
// further permissions can be added as needed // 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 ```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. * API Key is invalid or does not have the needed permission for the request.
* invalidBody * invalidBody
* Body of the request is malformed. * Body of the request is malformed.
* badRequest
* Some part of your request is invalid
* internal * internal
* Server-side issue. * Server-side issue.
### Log ### Count
API Key must have the `log` permission. API Key must have the `Count` permission.
Request: 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 ### 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. 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 #### Create User
> TODO: Email user to confirm. > TODO: Email user to confirm.
@@ -164,11 +192,19 @@ If returned status is 401, the errorCode will be one of the following:
* password * password
* Password is to short or too long. * Password is to short or too long.
#### Delete User
Requires either the `management` permission or a management key.
Request:
> DELETE: /user/{userID}
#### Login #### Login
Request: Request:
> POST: /user > POST: /user/login
```json ```json
{ {
@@ -196,9 +232,21 @@ Possible `error` values:
* invalid * invalid
* Either the username or password is incorrect * 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 #### Report
@@ -227,7 +275,7 @@ Request:
> DELETE: /crash/{crashID} > DELETE: /crash/{crashID}
With "darkstormManagement" key: With management key:
> DELETE: /{appID}/crash/{crashID} > DELETE: /{appID}/crash/{crashID}
@@ -239,16 +287,16 @@ Request:
> POST: /crash/archive > POST: /crash/archive
With "darkstormManagement" key: With management key:
> POST: /{appID}/crash/{crashID} > POST: /{appID}/crash/archive
Request Body: Request Body:
```json ```json
{ {
error: "error", 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". platform: "all", // Limit the archive to a specific platform, or use "all".
} }
``` ```
+1 -1
View File
@@ -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. // An application interface. Both LogTable and CrashTable are optional, if they return nil then requests will be forbidden.
type App interface { type App interface {
AppID() string AppID() string
LogTable() LogTable CountTable() CountTable
CrashTable() CrashTable CrashTable() CrashTable
} }
+78 -29
View File
@@ -2,7 +2,6 @@ package darkstorm
import ( import (
"encoding/json" "encoding/json"
"errors"
"log" "log"
"net/http" "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) { func (b *Backend) deleteCrash(w http.ResponseWriter, r *http.Request) {
hdr, err := b.ParseHeader(r) hdr, err := b.VerifyHeader(w, r, "management", false)
if hdr.Key == nil || hdr.Key.Perm["management"] || errors.Is(err, ErrApiKeyUnauthorized) { if hdr == nil {
ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") if err == nil {
return log.Println("request key parsing error:", err)
} else if err != nil { }
ReturnError(w, http.StatusInternalServerError, "internal", "Server error")
return 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) { func (b *Backend) managementDeleteCrash(w http.ResponseWriter, r *http.Request) {
hdr, err := b.ParseHeader(r) hdr, err := b.VerifyHeader(w, r, "management", true)
if hdr.Key == nil || hdr.Key.Perm["management"] || errors.Is(err, ErrApiKeyUnauthorized) { if hdr == nil {
ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") if err == nil {
return log.Println("request key parsing error:", err)
} else if err != nil { }
ReturnError(w, http.StatusInternalServerError, "internal", "Server error")
return 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) { func (b *Backend) archiveCrash(w http.ResponseWriter, r *http.Request) {
hdr, err := b.ParseHeader(r) hdr, err := b.VerifyHeader(w, r, "management", false)
if hdr.Key == nil || hdr.Key.Perm["management"] { if hdr == nil {
w.WriteHeader(http.StatusUnauthorized) if err == nil {
log.Println("request key parsing error:", err)
}
return return
} }
if err != nil { defer r.Body.Close()
//TODO 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 return
} }
//TODO b.actualCrashArchive(w, b.GetApp(hdr.Key), toArchive)
} }
func (b *Backend) managementArchiveCrash(w http.ResponseWriter, r *http.Request) { func (b *Backend) managementArchiveCrash(w http.ResponseWriter, r *http.Request) {
hdr, err := b.ParseHeader(r) hdr, err := b.VerifyHeader(w, r, "management", true)
if hdr.Key == nil || hdr.Key.Perm["management"] { if hdr == nil {
w.WriteHeader(http.StatusUnauthorized) if err == nil {
log.Println("request key parsing error:", err)
}
return return
} }
if err != nil { appID := r.PathValue("appID")
//TODO ap := b.apps[appID]
if ap == nil || appID == "" {
ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request")
return 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()
}
}
+8 -3
View File
@@ -37,7 +37,7 @@ func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) {
if ext, is := apps[i].(ExtendedApp); is { if ext, is := apps[i].(ExtendedApp); is {
b.m.HandleFunc("/"+apps[i].AppID()+"/", ext.Extension) b.m.HandleFunc("/"+apps[i].AppID()+"/", ext.Extension)
} }
if !hasLog && apps[i].LogTable() != nil { if !hasLog && apps[i].CountTable() != nil {
hasLog = true hasLog = true
} }
if !hasCrash && apps[i].CrashTable() != nil { if !hasCrash && apps[i].CrashTable() != nil {
@@ -45,7 +45,8 @@ func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) {
} }
} }
if hasLog { 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 { if hasCrash {
b.m.HandleFunc("POST /crash", b.reportCrash) b.m.HandleFunc("POST /crash", b.reportCrash)
@@ -61,7 +62,7 @@ func (b *Backend) cleanupLoop() {
oldTim := time.Now().Add(-30 * 24 * time.Hour) oldTim := time.Now().Add(-30 * 24 * time.Hour)
old := (oldTim.Year() * 10000) + (int(oldTim.Month()) * 100) + oldTim.Day() old := (oldTim.Year() * 10000) + (int(oldTim.Month()) * 100) + oldTim.Day()
for _, a := range b.apps { for _, a := range b.apps {
tab := a.LogTable() tab := a.CountTable()
if tab == nil { if tab == nil {
continue continue
} }
@@ -74,12 +75,16 @@ func (b *Backend) EnableManagementKey(managementID string) {
b.managementKeyID = managementID b.managementKeyID = managementID
b.m.HandleFunc("DELETE /{appID}/crash/{crashID}", b.managementDeleteCrash) b.m.HandleFunc("DELETE /{appID}/crash/{crashID}", b.managementDeleteCrash)
b.m.HandleFunc("POST /{appID}/crash/archive", b.managementArchiveCrash) 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) { func (b *Backend) AddUserAuth(userTable Table[User], privKey, pubKey []byte) {
b.userTable = userTable b.userTable = userTable
b.jwtPriv = privKey b.jwtPriv = privKey
b.jwtPub = pubKey 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) { func (b *Backend) HandleFunc(pattern string, h http.HandlerFunc) {
+5 -4
View File
@@ -19,17 +19,18 @@ type Table[T IDStruct] interface {
PartUpdate(ID string, update map[string]any) error PartUpdate(ID string, update map[string]any) error
} }
type LogTable interface { type CountTable interface {
Table[Log] Table[CountLog]
// Remove all Log items that have a Log.Date value less then the given value. // Remove all Log items that have a CountLog.Date value less then the given value.
RemoveOldLogs(date int) RemoveOldLogs(date int)
Count(platform string) int
} }
type CrashTable interface { type CrashTable interface {
Table[CrashReport] Table[CrashReport]
// Move a crash type to archive. All instances that perfectly match that appear in CrashReport.Individual should be deleted. // 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. // If a CrashReport ends up with an empty Individual array it should also be deleted.
Archive(ArchivedCrash) Archive(ArchivedCrash) error
IsArchived(IndividualCrash) bool IsArchived(IndividualCrash) bool
// Add the IndividualCrash report to the crash table. If a CrashReport exists that matches, then it gets added to CrashReport.Individual. // 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. // If an IndividualCrash exists that is a perfect match, Count is incremented instead of adding it to the array.
+1 -1
View File
@@ -15,7 +15,7 @@ var (
) )
type ParsedHeader struct { type ParsedHeader struct {
User *ReqUser User *ReqestUser
Key *ApiKey Key *ApiKey
} }
+36 -5
View File
@@ -1,17 +1,48 @@
package darkstorm package darkstorm
import "net/http" import (
"log"
"net/http"
)
type Log struct { type CountLog struct {
ID string ID string
Platform string Platform string
Date int Date int
} }
func (l Log) GetID() string { func (c CountLog) GetID() string {
return l.ID 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 //TODO
} }
+50 -27
View File
@@ -24,6 +24,24 @@ func generateSalt() (string, error) {
return base64.RawStdEncoding.EncodeToString(out), err 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 { type User struct {
Perm map[string]string `json:"perm" bson:"perm"` Perm map[string]string `json:"perm" bson:"perm"`
ID string `json:"id" bson:"_id"` ID string `json:"id" bson:"_id"`
@@ -59,27 +77,12 @@ func NewUser(username, password, email string) (User, error) {
return u, nil 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 { func (u User) GetID() string {
return u.ID return u.ID
} }
func (u User) toReqUser() *ReqUser { func (u User) toReqUser() *ReqestUser {
return &ReqUser{ return &ReqestUser{
Perm: u.Perm, Perm: u.Perm,
ID: u.ID, ID: u.ID,
Username: u.Username, Username: u.Username,
@@ -114,7 +117,7 @@ type createUserReturn struct {
Token string `json:"token"` 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) hdr, err := b.VerifyHeader(w, r, "user", false)
if hdr == nil { if hdr == nil {
if err == nil { if err == nil {
@@ -164,7 +167,7 @@ func (b *Backend) CreateUser(w http.ResponseWriter, r *http.Request) {
} }
var ret createUserReturn var ret createUserReturn
ret.Username = u.Username ret.Username = u.Username
ret.Token, err = b.generateJWT(u.toReqUser()) ret.Token, err = b.GenerateJWT(u.toReqUser())
if err != nil { if err != nil {
ReturnError(w, http.StatusInternalServerError, "internal", "Server error") ReturnError(w, http.StatusInternalServerError, "internal", "Server error")
return return
@@ -173,6 +176,27 @@ func (b *Backend) CreateUser(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(ret) 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 { type loginRequest struct {
Username string Username string
Password string Password string
@@ -184,13 +208,12 @@ type loginReturn struct {
Timeout int64 `json:"timeout"` Timeout int64 `json:"timeout"`
} }
func (b *Backend) Login(w http.ResponseWriter, r *http.Request) { func (b *Backend) login(w http.ResponseWriter, r *http.Request) {
hdr, err := b.ParseHeader(r) hdr, err := b.VerifyHeader(w, r, "user", false)
if hdr.Key == nil || !hdr.Key.Perm["user"] || errors.Is(err, ErrApiKeyUnauthorized) { if hdr == nil {
ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") if err == nil {
return log.Println("request key parsing error:", err)
} else if err != nil { }
ReturnError(w, http.StatusInternalServerError, "internal", "Server error")
return return
} }
defer r.Body.Close() defer r.Body.Close()
@@ -224,7 +247,7 @@ func (b *Backend) Login(w http.ResponseWriter, r *http.Request) {
return return
} }
if u.Password == hash { if u.Password == hash {
ret.Token, err = b.generateJWT(u.toReqUser()) ret.Token, err = b.GenerateJWT(u.toReqUser())
if err != nil { if err != nil {
ReturnError(w, http.StatusInternalServerError, "internal", "Server error") ReturnError(w, http.StatusInternalServerError, "internal", "Server error")
return return