backend

package
v0.0.0-...-1694169 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 27, 2024 License: MIT Imports: 16 Imported by: 0

README

Darkstorm Backend

This is a purposefully "simple" application backend made specifically for my apps. It's purpose is to collect minimal (only what's absolutely necessary) amounts of data while still fulfilling all my needs. I've found that other, off the shelf options such as Firebase are a bit heavy on the data collection. Plus I like to make things :P.

DB Structure

API Key
{
  id: "API Key",
  appID: "appID",
  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
    count: true, // count users
    crash: true, // crash reports
    management: false, // managing
    // further permissions can be added as needed
  },
  allowedOrigins:[
  	"http://foo.bar" // Request with this origin header is considered to be under this key.
  ]
}

Optionally you can set a special AppID to be a management key. Setting a management key enables management requests.

Count log
{
  id: "UUID",
  platform: "android",
  Date: 20240519 // YYYYMMDD as int
}
User

Users are stored per backend and not per app.

{
  id: "uuid",
  username: "username",
  password: "hashed password",
  salt: "password salt",
  email: "email",
  fails: 0, // number of failed attemps in a row.
  timeout: 0, // unix timestamp (seconds) when current timeout ends.
  passwordChange: 0, // unix timestamp (seconds) of last password change
  perm: {
    appID: "user", // Optional. Apps should have a default permission level if thier appID is not in perm.
  }
}
Crash Reports
Individual Report
{
  count: 1, // We do not store duplicates. If a duplicate does occur
  platform: "android",
  version: "v1.0.0", // Application version
  error: "error",
  stack: "stacktrace"
}
Crashes
{
  id: "UUID",
  error: "error",
  firstLine: "first line of error",
  individual: [
    // Individual Crash Reports
  ]
}

Requests

Standard Header

Any request might or might not need these headers. These values can be authenticated via the ParseHeader function.

{
  X-API-Key: "{API Key}",
  Authorization: "Bearer {JWT Token}" // No built-in functions require a JWT Token, but may be required by specific implementations.
}
Error Response

If an error status code is returned then the body will be as follows.

{
  errorCode: "Error value for internal use",
  errorMsg: "User error message", //This message is meant to be displayed to the user. May be empty.
}

errorCode's returned from the main library:

  • misconfigured
    • Backend is configured incorrectly (such as App returning nil crash table, but key has crash permission)
  • invalidKey
    • API Key is invalid or does not have the needed permission for the request.
  • invalidBody
    • Body of the request is malformed.
  • unauthorized
    • User is not authorized for the given task or no user token is given.
  • badRequest
    • Some part of your request is invalid
  • internal
    • Server-side issue.
Count

API Key must have the count permission.

Request:

POST: /count

{
  id: "uuid", // Should be an empty string on first request. If invalid or too old, a new UUID will be returned.
  platform: "web"
}

Returns:

{
  id: "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:

{
  count: 0
}
Users

TODO: Add the ability to create users and log-in through third-parties (such as Google).

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.

TODO: Screen username for offensive words and phrases.

Request:

POST: /user/create

{
  username: "Username",
  password: "Password", // Allowed length: 12-128
  email: "Email",
}

Return:

{
  username: "Username",
  token: "JWT Token"
}

If returned status is 401, the errorCode will be one of the following:

  • taken
    • Username or email is already taken
  • usernameDisallowed
    • Username is not allowed (due to offensive words/phrases)
  • 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/login

{
  username: "Username",
  password: "Password",
}

Return:

{
  token: "JWT Token",
  error: "Error",
  timeout: 0, // login attempt timeout remaining (in seconds). If non-zero, token will be empty.
}

token and error are mutually exclusive.

Possible error values:

  • timeout
    • Account is currently timed-out. The timeout value will be non-zero.
  • invalid
    • Either the username or password is incorrect
Change Password

Request:

POST: /user/changepassword

{
  token: "JWT Token",
  old: "Old Password",
  new: "New Password"
}
Crash Report
Report

API Key must have the crash permission.

Request:

POST: /crash

Request Body:

{
  id: "UUID", // This is an ignored value, but it is highly recommended to include it to prevent reporting the same crash multiple times.
  platform: "android",
  appVersion: "v1.0.0",
  error: "error",
  stack: "stacktrace"
}
Delete

API Key must have the management permission.

Request:

DELETE: /crash/{crashID}

With management key:

DELETE: /{appID}/crash/{crashID}

Archive

Archive an error, preventing error with these values to be ignored in the future. API Key must have the management permission.

Request:

POST: /crash/archive

With management key:

POST: /{appID}/crash/archive

Request Body:

{
  error: "error",
  stack: "full stacktrace", // Archives will only match against a perfect match.
  platform: "all", // Limit the archive to a specific platform, or use "all".
}

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrApiKeyUnauthorized = errors.New("api key present but invalid")
	ErrTokenUnauthorized  = errors.New("token present but invalid")
)
View Source
var (
	ErrLoginTimeout   = errors.New("user is timed out")
	ErrLoginIncorrect = errors.New("username or password is incorrect")
)
View Source
var (
	ErrNotFound = errors.New("no matches found in table")
)
View Source
var (
	ErrPasswordLength = errors.New("password length must be 12-128")
)

Functions

func ReturnError

func ReturnError(w http.ResponseWriter, status int, code, msg string)

Return an error response with the given status code, code, and message.

Types

type ApiKey

type ApiKey struct {
	Perm           map[string]bool `json:"perm" bson:"perm"`
	ID             string          `json:"id" bson:"_id" valkey:",key"`
	AppID          string          `json:"appID" bson:"appID"`
	Death          int64           `json:"death" bson:"death"`
	AllowedOrigins []string        `json:"allowedOrigins" bson:"allowedOrigins"`
}

func (ApiKey) GetID

func (k ApiKey) GetID() string

type App

type App interface {
	AppID() string
	CountTable() CountTable
	CrashTable() CrashTable
}

An application interface. Both LogTable and CrashTable are optional, if they return nil then requests will be forbidden.

func NewSimpleApp

func NewSimpleApp(appID string, countTable CountTable, crashTable CrashTable) App

type ArchivedCrash

type ArchivedCrash struct {
	Error    string `json:"error" bson:"error"`
	Stack    string `json:"stack" bson:"stack"`
	Platform string `json:"platform" bson:"platform"`
}

type Backend

type Backend struct {
	// contains filtered or unexported fields
}

A simple backend that handles user authentication, user count, and crash reports.

func NewBackend

func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error)

Create a new Backend with the given apps. keyTable must be specified.

func (*Backend) AddCorsAddress

func (b *Backend) AddCorsAddress(corsAddr string)

Enable CORS for with the given cors address

func (*Backend) AddUserAuth

func (b *Backend) AddUserAuth(userTable Table[User], privKey, pubKey []byte)

Enables user creation and authentication.

func (*Backend) EnableManagementKey

func (b *Backend) EnableManagementKey(managementID string)

Enables the use of a management API key for crash and count.

func (*Backend) GenerateJWT

func (b *Backend) GenerateJWT(r *ReqestUser) (string, error)

func (*Backend) GetApp

func (b *Backend) GetApp(a *ApiKey) App

Try to get the App associated with the given ApiKey. Returns nil if not found.

func (*Backend) HandleFunc

func (b *Backend) HandleFunc(pattern string, h http.HandlerFunc)

Add values to the Backend's underlying ServeMux

func (*Backend) ParseHeader

func (b *Backend) ParseHeader(r *http.Request) (*ParsedHeader, error)

Parses the X-API-Key and Authorization headers. If the API Key provided but invalid (either due to expiring or isn't found), ErrApiKeyUnauthorized is returned. If the Authorization header is present but invalid, ErrTokenUnauthorized is returned. NOTE: An invalid apiKey will cause a nil return, but a invalid token will not. Token parsing is only

func (*Backend) ServeHTTP

func (b *Backend) ServeHTTP(w http.ResponseWriter, r *http.Request)

http.Handler

func (*Backend) TryLogin

func (b *Backend) TryLogin(ctx context.Context, username, password string) (User, error)

Tries to login with the given username and password. If the user exists, but is timed out, the user is still returned.

func (*Backend) VerifyHeader

func (b *Backend) VerifyHeader(w http.ResponseWriter, r *http.Request, keyPerm string, allowManagementKey bool) (*ParsedHeader, error)

Similiar to ParseHeader, but with key checking and automatic error returns. Guarentess Backend.GetApp is non-nil Checks that the key is a management key (not management permission and if allowManagement is true) or that it has the necessary permission. If the check if failed, ReturnError will be called and the returned *ParsedHeader will be nil. If token is present but invalid, no error will be returned just ParsedHeader.User will be nil. The error return will only be populated on "internal" errors and should *probably* be logged.

This function does not check the Key's appID so after calling VerifyHeader it's recommended to check the Key's appID.

func (*Backend) VerifyUser

func (b *Backend) VerifyUser(ctx context.Context, token string) (*User, error)

type CallbackApp

type CallbackApp interface {
	App
	AddBackend(*Backend)
}

Provides an App access to it's parent *Backend. This is called only once, while setting up the Backend.

type CountLog

type CountLog struct {
	ID       string `json:"id" bson:"_id"`
	Platform string `json:"platform" bson:"platform"`
	Date     int    `json:"date" bson:"date"`
}

func (CountLog) GetID

func (c CountLog) GetID() string

type CountTable

type CountTable interface {
	Table[CountLog]
	// Remove all Log items that have a CountLog.Date value less then the given value.
	RemoveOldLogs(ctx context.Context, date int) error
	// Get count. If platform is an empty string or "all", the full count should be given
	Count(ctx context.Context, platform string) (int, error)
}

type CrashFilterApp

type CrashFilterApp interface {
	App
	ShouldAddCrash(context.Context, IndividualCrash) bool
}

Allows for an App to filter crashes before they get added to the DB, such as making sure the crash is from the correct version.

type CrashReport

type CrashReport struct {
	ID         string            `json:"id" bson:"_id"`
	Error      string            `json:"error" bson:"error"`
	FirstLine  string            `json:"firstLine" bson:"firstLine"`
	Individual []IndividualCrash `json:"individual" bson:"individual"`
}

func (CrashReport) GetID

func (c CrashReport) GetID() string

type CrashTable

type CrashTable interface {
	Table[CrashReport]
	// Move a crash type to archive. Crashes that match the archived crash will be automatically removed from the CrashTable.
	Archive(context.Context, ArchivedCrash) error
	IsArchived(context.Context, 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.
	InsertCrash(context.Context, IndividualCrash) error
}

type ExtendedApp

type ExtendedApp interface {
	App
	Extension(*http.ServeMux)
}

Allows an app more flexibility by directly interfacing with the backend's mux

type IDStruct

type IDStruct interface {
	GetID() string
}

type IndividualCrash

type IndividualCrash struct {
	Platform string `json:"platform" bson:"platform"`
	Version  string `json:"version" bson:"version"`
	Error    string `json:"error" bson:"error"`
	Stack    string `json:"stack" bson:"stack"`
	Count    int    `json:"count" bson:"count"`
}

type ParsedHeader

type ParsedHeader struct {
	User *ReqestUser
	Key  *ApiKey
}

type ReqestUser

type ReqestUser struct {
	Perm     map[string]string
	ID       string
	Username string
}

type Table

type Table[T IDStruct] interface {
	Get(ctx context.Context, ID string) (data T, err error)
	Find(ctx context.Context, values map[string]any) ([]T, error)
	Insert(ctx context.Context, data T) error
	Remove(ctx context.Context, ID string) error
	FullUpdate(ctx context.Context, ID string, data T) error
	PartUpdate(ctx context.Context, ID string, update map[string]any) error
}

type User

type User struct {
	Perm           map[string]string `json:"perm" bson:"perm"`
	ID             string            `json:"id" bson:"_id"`
	Username       string            `json:"username" bson:"username"`
	Password       string            `json:"password" bson:"password"`
	Salt           string            `json:"salt" bson:"salt"`
	Email          string            `json:"email" bson:"email"`
	Fails          int               `json:"fails" bson:"fails"`
	Timeout        int64             `json:"timeout" bson:"timeout"`
	PasswordChange int64             `json:"passwordChange" bson:"passwordChange"`
}

func NewUser

func NewUser(username, password, email string) (User, error)

func (User) GetID

func (u User) GetID() string

func (User) HashPassword

func (u User) HashPassword(password string) (string, error)

func (User) ToReqUser

func (u User) ToReqUser() *ReqestUser

func (User) ValidatePassword

func (u User) ValidatePassword(password string) (bool, error)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL