diff --git a/go.mod b/go.mod index 4c16ab9..9131155 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,7 @@ require ( golang.org/x/crypto v0.23.0 ) -require golang.org/x/sys v0.20.0 // indirect +require ( + github.com/golang-jwt/jwt/v5 v5.2.1 + golang.org/x/sys v0.20.0 // indirect +) diff --git a/go.sum b/go.sum index 4297843..c5c1062 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= diff --git a/internal/darkstorm_backend/README.md b/internal/darkstorm_backend/README.md index 10d99db..5763724 100644 --- a/internal/darkstorm_backend/README.md +++ b/internal/darkstorm_backend/README.md @@ -15,11 +15,22 @@ This is a purposefully "simple" application backend made specifically for _my_ a user: true, // create and login users log: true, // log users crash: true, // crash reports + management: false, // managing // further permissions can be added as needed } } ``` +### DB Log + +```json +{ + id: "UUID", + platform: "android", + Date: 20240519 // YYYYMMD +} +``` + ### User Users are stored per backend and not per app. @@ -88,6 +99,14 @@ If an error status code is returned then the body will be as follows. } ``` +### Log + +API Key must have the `log` permission. + +Request: + +> POST: /log + ### Users > TODO: Add the ability to create users and log-in through third-parties (such as Google). @@ -107,7 +126,7 @@ Request: ```json { username: "Username", - password: "Password", // Password must be + password: "Password", // Allowed length: 12-128 email: "Email", } ``` @@ -158,11 +177,15 @@ Return: ### Crash Report -Crash reports require the `X-API-Key` header and the key must match the URL's appID and have the `crash` permission +> TODO: Archive a crash to prevent it being reported again. + +#### Report + +API Key must have the `crash` permission. Request: -> POST: /{appID}/crash +> POST: /crash ```json { @@ -172,3 +195,11 @@ Request: stack: "stacktrace" } ``` + +#### Delete + +API Key must have the `management` permission. + +Request: + +> DELETE: /crash/{crashID} diff --git a/internal/darkstorm_backend/app.go b/internal/darkstorm_backend/app.go index 4ebdc4b..96d9108 100644 --- a/internal/darkstorm_backend/app.go +++ b/internal/darkstorm_backend/app.go @@ -1,11 +1,6 @@ package darkstorm type App interface { - //TODO -} - -type CrashApp interface { - App - AddCrash(CrashReport) - // TODO + LogTable() Table[Log] + CrashTable() CrashTable } diff --git a/internal/darkstorm_backend/crash.go b/internal/darkstorm_backend/crash.go index cc87fa2..56cb458 100644 --- a/internal/darkstorm_backend/crash.go +++ b/internal/darkstorm_backend/crash.go @@ -1,5 +1,7 @@ package darkstorm +import "net/http" + type IndividualCrash struct { Platform string Error string @@ -13,3 +15,40 @@ type CrashReport struct { FirstLine string Individual []IndividualCrash } + +func (c CrashReport) GetID() string { + return c.ID +} + +type crashReq struct { + ID string + Platform string + Error string + Stack string +} + +func (b *Backend) ReportCrash(w http.ResponseWriter, r *http.Request) { + hdr, err := b.ParseHeader(r) + if hdr.k == nil || hdr.k.Perm["crash"] { + w.WriteHeader(http.StatusUnauthorized) + return + } + if err != nil { + //TODO + return + } + //TODO +} + +func (b *Backend) DeleteCrash(w http.ResponseWriter, r *http.Request) { + hdr, err := b.ParseHeader(r) + if hdr.k == nil || hdr.k.Perm["management"] { + w.WriteHeader(http.StatusUnauthorized) + return + } + if err != nil { + //TODO + return + } + //TODO +} diff --git a/internal/darkstorm_backend/darkstorm.go b/internal/darkstorm_backend/darkstorm.go index 8b49e57..efa85bc 100644 --- a/internal/darkstorm_backend/darkstorm.go +++ b/internal/darkstorm_backend/darkstorm.go @@ -1,7 +1,90 @@ package darkstorm -import "net/http" +import ( + "crypto/ed25519" + "errors" + "net/http" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var ( + ErrApiKeyUnauthorized = errors.New("api key invalid") + ErrTokenUnauthorized = errors.New("token invalid") +) type Backend struct { - http.ServeMux + userTable Table[User] + keyTable Table[Key] + m *http.ServeMux + jwtPriv ed25519.PrivateKey + jwtPub ed25519.PublicKey + apps []App +} + +func NewBackend(keyTable Table[Key], apps ...App) (*Backend, error) { + b := &Backend{ + keyTable: keyTable, + m: &http.ServeMux{}, + apps: apps, + } + //TODO: register paths to the mux + b.startCleanupLoop() + return b, nil +} + +func (b *Backend) AddUserAuth(userTable Table[User], privKey, pubKey []byte) { + b.userTable = userTable + b.jwtPriv = privKey + b.jwtPub = pubKey +} + +func (b *Backend) HandleFunc(pattern string, h http.HandlerFunc) { + b.m.HandleFunc(pattern, h) +} + +func (b *Backend) startCleanupLoop() { + go func() { + for range time.Tick(6 * time.Hour) { + //TODO + } + }() +} + +type ParsedHeader struct { + u *ReqUser + k *Key +} + +func (b *Backend) ParseHeader(r *http.Request) (ParsedHeader, error) { + out := ParsedHeader{} + key := r.Header.Get("X-API-Key") + token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") + if key != "" { + apiKey, err := b.keyTable.Get(key) + if err != nil { + return out, errors.Join(ErrApiKeyUnauthorized, err) + } + out.k = &apiKey + } + if token != "" && b.userTable != nil { + t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { + return b.jwtPub, nil + }, jwt.WithIssuer("darkstorm.tech"), jwt.WithExpirationRequired()) + if err != nil { + return out, errors.Join(ErrTokenUnauthorized, err) + } + sub, err := t.Claims.GetSubject() + if err != nil { + return out, errors.Join(ErrTokenUnauthorized, err) + } + usr, err := b.userTable.Get(sub) + if err != nil{ + return out, errors.Join(ErrTokenUnauthorized, err) + } + + } + return out, nil } diff --git a/internal/darkstorm_backend/db.go b/internal/darkstorm_backend/db.go new file mode 100644 index 0000000..87a16be --- /dev/null +++ b/internal/darkstorm_backend/db.go @@ -0,0 +1,23 @@ +package darkstorm + +import "errors" + +var ( + ErrIDNotFound = errors.New("id not found in table") +) + +type IDStruct interface { + GetID() string +} + +type Table[T IDStruct] interface { + Get(ID string) (data T, err error) + Insert(data T) error + Update(data T) error + Remove(ID string) +} + +type CrashTable interface { + Table[CrashReport] + InsertCrash(report IndividualCrash) error +} diff --git a/internal/darkstorm_backend/key.go b/internal/darkstorm_backend/key.go new file mode 100644 index 0000000..74dd5e9 --- /dev/null +++ b/internal/darkstorm_backend/key.go @@ -0,0 +1,12 @@ +package darkstorm + +type Key struct { + Perm map[string]bool + ID string + AppID string + Death int +} + +func (k Key) GetID() string { + return k.ID +} diff --git a/internal/darkstorm_backend/log.go b/internal/darkstorm_backend/log.go new file mode 100644 index 0000000..4976e51 --- /dev/null +++ b/internal/darkstorm_backend/log.go @@ -0,0 +1,11 @@ +package darkstorm + +type Log struct { + ID string + Platform string + Date int +} + +func (l Log) GetID() string { + return l.ID +} diff --git a/internal/darkstorm_backend/user.go b/internal/darkstorm_backend/user.go index 439299d..a456306 100644 --- a/internal/darkstorm_backend/user.go +++ b/internal/darkstorm_backend/user.go @@ -3,11 +3,17 @@ package darkstorm import ( "crypto/rand" "encoding/base64" + "errors" + "net/http" "github.com/google/uuid" "golang.org/x/crypto/argon2" ) +var ( + ErrPasswordLength = errors.New("password length must be 12-128") +) + func generateSalt() (string, error) { out := make([]byte, 16) _, err := rand.Read(out) @@ -24,7 +30,16 @@ type User struct { PasswordChange uint64 } +type ReqUser struct { + Perm map[string]string + ID string + Username string +} + func NewUser(username, password, email string) (*User, error) { + if len(password) < 12 || len(password) > 128 { + return nil, ErrPasswordLength + } id, err := uuid.NewV7() if err != nil { return nil, err @@ -44,7 +59,19 @@ func NewUser(username, password, email string) (*User, error) { return out, err } -func (u *User) HashPassword(password string) (string, error) { +func (u User) GetID() string { + return u.ID +} + +func (u User) toReqUser() ReqUser { + return ReqUser{ + Perm: u.Perm, + ID: u.ID, + Username: u.Username, + } +} + +func (u User) HashPassword(password string) (string, error) { salt, err := base64.RawStdEncoding.DecodeString(u.Salt) if err != nil { return "", err @@ -53,10 +80,39 @@ func (u *User) HashPassword(password string) (string, error) { return base64.RawStdEncoding.EncodeToString(res), nil } -func (u *User) ValidatePassword(password string) (bool, error) { +func (u User) ValidatePassword(password string) (bool, error) { hsh, err := u.HashPassword(password) if err != nil { return false, err } return hsh == u.Password, nil } + +type createUserRequest struct { + Username string + Password string + Email string +} + +type createUserReturn struct { + Username string + Token string +} + +func (b *Backend) CreateUser(w http.ResponseWriter, r *http.Request) { + //TODO +} + +type loginRequest struct { + Username string + Password string +} + +type loginReturn struct { + Token string + Timeout int +} + +func (b *Backend) Login(w http.ResponseWriter, r *http.Request) { + //TODO +} diff --git a/internal/db/interface.go b/internal/db/interface.go deleted file mode 100644 index 8a2770a..0000000 --- a/internal/db/interface.go +++ /dev/null @@ -1,5 +0,0 @@ -package db - -type DB interface { - //TODO -}