initial commit

This commit is contained in:
Sam 2022-05-02 17:19:37 +02:00
commit 5a75f99720
20 changed files with 2239 additions and 0 deletions

55
backend/server/auth.go Normal file
View file

@ -0,0 +1,55 @@
package server
import (
"context"
"net/http"
"gitlab.com/1f320/pronouns/backend/server/auth"
)
// maybeAuth is a globally-used middleware.
func (s *Server) maybeAuth(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
next.ServeHTTP(w, r)
return
}
claims, err := s.Auth.Claims(token)
if err != nil {
// if we get here, a token was supplied but it's invalid--return an error
}
ctx := context.WithValue(r.Context(), ctxKeyClaims, claims)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
}
// MustAuth makes a valid token required
func MustAuth(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
_, ok := ClaimsFromContext(r.Context())
if !ok {
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// ClaimsFromContext returns the auth.Claims in the context, if any.
func ClaimsFromContext(ctx context.Context) (auth.Claims, bool) {
v := ctx.Value(ctxKeyClaims)
if v == nil {
return auth.Claims{}, false
}
claims, ok := v.(auth.Claims)
return claims, ok
}

View file

@ -0,0 +1,81 @@
package auth
import (
"encoding/base64"
"fmt"
"os"
"time"
"emperror.dev/errors"
"github.com/golang-jwt/jwt/v4"
"github.com/rs/xid"
"gitlab.com/1f320/pronouns/backend/log"
)
// Claims are the claims used in a token.
type Claims struct {
UserID xid.ID `json:"sub"`
jwt.RegisteredClaims
}
type Verifier struct {
key []byte
}
func New() *Verifier {
raw := os.Getenv("HMAC_KEY")
if raw == "" {
log.Fatal("$HMAC_KEY is not set")
}
key, err := base64.URLEncoding.DecodeString(raw)
if err != nil {
log.Fatal("$HMAC_KEY is not a valid base 64 string")
}
return &Verifier{key: key}
}
const expireDays = 30
// CreateToken creates a token for the given user ID.
// It expires after 30 days.
func (v *Verifier) CreateToken(userID xid.ID) (string, error) {
now := time.Now()
expires := now.Add(expireDays * 24 * time.Hour)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "pronouns",
ExpiresAt: jwt.NewNumericDate(expires),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
},
})
return token.SignedString(v.key)
}
// Claims parses the given token and returns its Claims.
// If the token is invalid, returns an error.
func (v *Verifier) Claims(token string) (c Claims, err error) {
parsed, err := jwt.ParseWithClaims(token, &Claims{}, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf(`unexpected signing method "%v"`, t.Header["alg"])
}
return v.key, nil
})
if err != nil {
return c, errors.Wrap(err, "parsing token")
}
if c, ok := parsed.Claims.(*Claims); ok && parsed.Valid {
return *c, nil
}
return c, fmt.Errorf("unknown claims type %T", parsed.Claims)
}

74
backend/server/errors.go Normal file
View file

@ -0,0 +1,74 @@
package server
import (
"fmt"
"net/http"
"github.com/go-chi/render"
"gitlab.com/1f320/pronouns/backend/log"
)
// WrapHandler wraps a modified http.HandlerFunc into a stdlib-compatible one.
// The inner HandlerFunc additionally returns an error.
func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := hn(w, r)
if err != nil {
// if the function returned an API error, just render that verbatim
// we can assume that it also logged the error (if that was needed)
if apiErr, ok := err.(APIError); ok {
apiErr.prepare()
render.Status(r, apiErr.Status)
render.JSON(w, r, apiErr)
return
}
// otherwise, we log the error and return an internal server error message
log.Errorf("error in http handler: %v", err)
apiErr := APIError{Code: ErrInternalServerError}
apiErr.prepare()
render.Status(r, apiErr.Status)
render.JSON(w, r, apiErr)
}
}
}
// APIError is an object returned by the API when an error occurs.
// It implements the error interface and can be returned by handlers.
type APIError struct {
Code int `json:"code"`
Message string `json:"message,omitempty"`
// Status is
Status int `json:"-"`
}
func (e APIError) Error() string {
return fmt.Sprintf("%s (code: %d)", e.Message, e.Code)
}
func (e *APIError) prepare() {
if e.Status == 0 {
e.Status = errCodeStatuses[e.Code]
}
if e.Message == "" {
e.Message = errCodeMessages[e.Code]
}
}
// Error code constants
const (
ErrInternalServerError = 500 // catch-all code for unknown errors
)
var errCodeMessages = map[int]string{
ErrInternalServerError: "Internal server error",
}
var errCodeStatuses = map[int]int{
ErrInternalServerError: http.StatusInternalServerError,
}

49
backend/server/server.go Normal file
View file

@ -0,0 +1,49 @@
package server
import (
"os"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"gitlab.com/1f320/pronouns/backend/db"
"gitlab.com/1f320/pronouns/backend/server/auth"
)
// Revision is the git commit, filled at build time
var Revision = "[unknown]"
type Server struct {
Router *chi.Mux
DB *db.DB
Auth *auth.Verifier
}
func New() (*Server, error) {
db, err := db.New(os.Getenv("DATABASE_URL"))
if err != nil {
return nil, err
}
s := &Server{
Router: chi.NewMux(),
DB: db,
Auth: auth.New(),
}
if os.Getenv("DEBUG") == "true" {
s.Router.Use(middleware.Logger)
}
s.Router.Use(middleware.Recoverer)
// enable authentication for all routes (but don't require it)
s.Router.Use(s.maybeAuth)
return s, nil
}
type ctxKey int
const (
ctxKeyClaims ctxKey = 1
)