better code
This commit is contained in:
parent
40c3406dcb
commit
4e72beb433
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -11,7 +11,7 @@
|
|||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"program": "${workspaceFolder}/cmd/party",
|
"program": "${workspaceFolder}/cmd/party",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"envFile": "${workspaceFolder}/.env",
|
"envFile": "${workspaceFolder}/.envrc",
|
||||||
"args": ["--env=development"]
|
"args": ["--env=development"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import(
|
import(
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@ -9,42 +8,19 @@ import(
|
|||||||
"party.at/party/cmd/party/common"
|
"party.at/party/cmd/party/common"
|
||||||
"party.at/party/internal/data"
|
"party.at/party/internal/data"
|
||||||
"party.at/party/internal/validator"
|
"party.at/party/internal/validator"
|
||||||
|
|
||||||
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Api struct {
|
type Api struct {
|
||||||
App *common.Application
|
App *common.Application
|
||||||
}
|
}
|
||||||
|
|
||||||
type contextKey string
|
|
||||||
|
|
||||||
const userKey contextKey = "user"
|
|
||||||
|
|
||||||
func SetUser(r *http.Request, user *data.User) *http.Request {
|
|
||||||
return r.WithContext(context.WithValue(r.Context(), userKey, user))
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetUser(r *http.Request) *data.User {
|
|
||||||
user, ok := r.Context().Value(userKey).(*data.User)
|
|
||||||
if !ok {
|
|
||||||
panic("missing user value in request context")
|
|
||||||
}
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) Authenticate(next http.Handler) http.Handler {
|
func (api *Api) Authenticate(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Add("Vary", "Authorization")
|
w.Header().Add("Vary", "Authorization")
|
||||||
|
|
||||||
authorizationHeader := r.Header.Get("Authorization")
|
authorizationHeader := r.Header.Get("Authorization")
|
||||||
if authorizationHeader == "" {
|
if authorizationHeader == "" {
|
||||||
r = SetUser(r, data.AnonymousUser)
|
r = common.SetUser(r, data.AnonymousUser)
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -84,357 +60,7 @@ func (api *Api) Authenticate(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
r = SetUser(r, user)
|
r = common.SetUser(r, user)
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type envelope map[string]interface{}
|
|
||||||
|
|
||||||
type apiError struct {
|
|
||||||
Code common.ErrorCode `json:"code,omitempty"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Details map[string]string `json:"details,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ── JSON helpers ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func (api *Api) writeJSON(w http.ResponseWriter, status int, data envelope, headers http.Header) error {
|
|
||||||
js, err := json.MarshalIndent(data, "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
js = append(js, '\n')
|
|
||||||
for key, value := range headers {
|
|
||||||
w.Header()[key] = value
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(status)
|
|
||||||
w.Write(js)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) readJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
|
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, 1048576)
|
|
||||||
dec := json.NewDecoder(r.Body)
|
|
||||||
dec.DisallowUnknownFields()
|
|
||||||
|
|
||||||
err := dec.Decode(dst)
|
|
||||||
if err != nil {
|
|
||||||
var syntaxError *json.SyntaxError
|
|
||||||
var unmarshalTypeError *json.UnmarshalTypeError
|
|
||||||
var invalidUnmarshalError *json.InvalidUnmarshalError
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case errors.As(err, &syntaxError):
|
|
||||||
return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset)
|
|
||||||
case errors.Is(err, io.ErrUnexpectedEOF):
|
|
||||||
return errors.New("body contains badly-formed JSON")
|
|
||||||
case errors.As(err, &unmarshalTypeError):
|
|
||||||
if unmarshalTypeError.Field != "" {
|
|
||||||
return fmt.Errorf("body contains incorrect JSON type for field %q", unmarshalTypeError.Field)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("body contains incorrect JSON type (at character %d)", unmarshalTypeError.Offset)
|
|
||||||
case errors.Is(err, io.EOF):
|
|
||||||
return errors.New("body must not be empty")
|
|
||||||
case strings.HasPrefix(err.Error(), "json: unknown field "):
|
|
||||||
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
|
|
||||||
return fmt.Errorf("body contains unknown key %s", fieldName)
|
|
||||||
case err.Error() == "http: request body too large":
|
|
||||||
return fmt.Errorf("body must not be larger than 1048576 bytes")
|
|
||||||
case errors.As(err, &invalidUnmarshalError):
|
|
||||||
panic(err)
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = dec.Decode(&struct{}{})
|
|
||||||
if err != io.EOF {
|
|
||||||
return errors.New("body must only contain a single JSON value")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) readIDParam(r *http.Request) (int64, error) {
|
|
||||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
||||||
if err != nil || id < 1 {
|
|
||||||
return 0, errors.New("invalid id parameter")
|
|
||||||
}
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) readString(qs url.Values, key, defaultValue string) string {
|
|
||||||
if s := qs.Get(key); s != "" {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) readInt(qs url.Values, key string, defaultValue int, v *validator.Validator) int {
|
|
||||||
s := qs.Get(key)
|
|
||||||
if s == "" {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
i, err := strconv.Atoi(s)
|
|
||||||
if err != nil {
|
|
||||||
v.AddError(key, "must be an integer value")
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Error responses ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func (api *Api) LogError(r *http.Request, err error) {
|
|
||||||
api.App.Logger.PrintError(err, map[string]string{
|
|
||||||
"request_method": r.Method,
|
|
||||||
"request_url": r.URL.String(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) errorResponse(w http.ResponseWriter, r *http.Request, status int, ae apiError) {
|
|
||||||
if err := common.WriteJSON(w, status, common.Envelope{"error": ae}, nil); err != nil {
|
|
||||||
api.App.LogError(r, err)
|
|
||||||
w.WriteHeader(500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) ServerErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
|
||||||
api.App.LogError(r, err)
|
|
||||||
api.errorResponse(w, r, http.StatusInternalServerError, apiError{
|
|
||||||
Message: "the server encountered a problem and could not process your request",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) NotFoundResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
api.errorResponse(w, r, http.StatusNotFound, apiError{
|
|
||||||
Message: "the requested resource could not be found",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) MethodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
api.errorResponse(w, r, http.StatusMethodNotAllowed, apiError{
|
|
||||||
Message: fmt.Sprintf("the %s method is not supported for this resource", r.Method),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) BadRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
|
|
||||||
api.errorResponse(w, r, http.StatusBadRequest, apiError{Message: err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) FailedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {
|
|
||||||
api.errorResponse(w, r, http.StatusUnprocessableEntity, apiError{
|
|
||||||
Code: common.ErrCodeValidationFailed,
|
|
||||||
Message: "validation failed",
|
|
||||||
Details: errors,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) EditConflictResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
api.errorResponse(w, r, http.StatusConflict, apiError{
|
|
||||||
Code: common.ErrCodeEditConflict,
|
|
||||||
Message: "unable to update the record due to an edit conflict, please try again",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) InvalidCredentialsResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
api.errorResponse(w, r, http.StatusUnauthorized, apiError{
|
|
||||||
Code: common.ErrCodeInvalidCredentials,
|
|
||||||
Message: "invalid authentication credentials",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) InvalidAuthenticationTokenResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("WWW-Authenticate", "Bearer")
|
|
||||||
api.errorResponse(w, r, http.StatusUnauthorized, apiError{
|
|
||||||
Code: common.ErrCodeInvalidAuthToken,
|
|
||||||
Message: "invalid or missing authentication token",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) RateLimitExceededResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
api.errorResponse(w, r, http.StatusTooManyRequests, apiError{
|
|
||||||
Message: "rate limit exceeded",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) AuthenticationRequiredResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
api.errorResponse(w, r, http.StatusUnauthorized, apiError{
|
|
||||||
Code: common.ErrCodeAuthRequired,
|
|
||||||
Message: "you must be authenticated to access this resource",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) InactiveAccountResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
api.errorResponse(w, r, http.StatusForbidden, apiError{
|
|
||||||
Code: common.ErrCodeInactiveAccount,
|
|
||||||
Message: "your user account must be activated to access this resource",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) NotPermittedResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
api.errorResponse(w, r, http.StatusForbidden, apiError{
|
|
||||||
Code: common.ErrCodeNotPermitted,
|
|
||||||
Message: "your user account doesn't have the necessary permissions to access this resource",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) AlreadyVotedResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
api.errorResponse(w, r, http.StatusConflict, apiError{
|
|
||||||
Code: common.ErrCodeAlreadyVoted,
|
|
||||||
Message: "your user account already voted for this issue",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) AlreadyBlindSignedResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
api.errorResponse(w, r, http.StatusConflict, apiError{
|
|
||||||
Code: common.ErrCodeAlreadyBlindSigned,
|
|
||||||
Message: "already requested a blind signature for this issue",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) BlindedVoteOutOfRangeResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
api.errorResponse(w, r, http.StatusUnprocessableEntity, apiError{
|
|
||||||
Code: common.ErrCodeBlindedVoteRange,
|
|
||||||
Message: "blinded_vote is out of valid range",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) InvalidSignatureResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
api.errorResponse(w, r, http.StatusUnprocessableEntity, apiError{
|
|
||||||
Code: common.ErrCodeInvalidSignature,
|
|
||||||
Message: "invalid signature",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *Api) VoteAlreadyCastResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
api.errorResponse(w, r, http.StatusConflict, apiError{
|
|
||||||
Code: common.ErrCodeVoteAlreadyCast,
|
|
||||||
Message: "this vote has already been cast",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// func (app *Application) errorResponse(w http.ResponseWriter, r *http.Request, status int, ae apiError) {
|
|
||||||
// if err := WriteJSON(w, status, Envelope{"error": ae}, nil); err != nil {
|
|
||||||
// app.LogError(r, err)
|
|
||||||
// w.WriteHeader(500)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) ServerErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
|
||||||
// app.LogError(r, err)
|
|
||||||
// app.errorResponse(w, r, http.StatusInternalServerError, apiError{
|
|
||||||
// Message: "the server encountered a problem and could not process your request",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) NotFoundResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// app.errorResponse(w, r, http.StatusNotFound, apiError{
|
|
||||||
// Message: "the requested resource could not be found",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) MethodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// app.errorResponse(w, r, http.StatusMethodNotAllowed, apiError{
|
|
||||||
// Message: fmt.Sprintf("the %s method is not supported for this resource", r.Method),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) BadRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
|
|
||||||
// app.errorResponse(w, r, http.StatusBadRequest, apiError{Message: err.Error()})
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) FailedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {
|
|
||||||
// app.errorResponse(w, r, http.StatusUnprocessableEntity, apiError{
|
|
||||||
// Code: ErrCodeValidationFailed,
|
|
||||||
// Message: "validation failed",
|
|
||||||
// Details: errors,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) EditConflictResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// app.errorResponse(w, r, http.StatusConflict, apiError{
|
|
||||||
// Code: ErrCodeEditConflict,
|
|
||||||
// Message: "unable to update the record due to an edit conflict, please try again",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) InvalidCredentialsResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// app.errorResponse(w, r, http.StatusUnauthorized, apiError{
|
|
||||||
// Code: ErrCodeInvalidCredentials,
|
|
||||||
// Message: "invalid authentication credentials",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) RateLimitExceededResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// app.errorResponse(w, r, http.StatusTooManyRequests, apiError{
|
|
||||||
// Message: "rate limit exceeded",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) AuthenticationRequiredResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// app.errorResponse(w, r, http.StatusUnauthorized, apiError{
|
|
||||||
// Code: ErrCodeAuthRequired,
|
|
||||||
// Message: "you must be authenticated to access this resource",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) InactiveAccountResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// app.errorResponse(w, r, http.StatusForbidden, apiError{
|
|
||||||
// Code: ErrCodeInactiveAccount,
|
|
||||||
// Message: "your user account must be activated to access this resource",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) NotPermittedResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// app.errorResponse(w, r, http.StatusForbidden, apiError{
|
|
||||||
// Code: ErrCodeNotPermitted,
|
|
||||||
// Message: "your user account doesn't have the necessary permissions to access this resource",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) AlreadyVotedResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// app.errorResponse(w, r, http.StatusConflict, apiError{
|
|
||||||
// Code: ErrCodeAlreadyVoted,
|
|
||||||
// Message: "your user account already voted for this issue",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) AlreadyBlindSignedResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// app.errorResponse(w, r, http.StatusConflict, apiError{
|
|
||||||
// Code: ErrCodeAlreadyBlindSigned,
|
|
||||||
// Message: "already requested a blind signature for this issue",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) BlindedVoteOutOfRangeResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// app.errorResponse(w, r, http.StatusUnprocessableEntity, apiError{
|
|
||||||
// Code: ErrCodeBlindedVoteRange,
|
|
||||||
// Message: "blinded_vote is out of valid range",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) InvalidSignatureResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// app.errorResponse(w, r, http.StatusUnprocessableEntity, apiError{
|
|
||||||
// Code: ErrCodeInvalidSignature,
|
|
||||||
// Message: "invalid signature",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (app *Application) VoteAlreadyCastResponse(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// app.errorResponse(w, r, http.StatusConflict, apiError{
|
|
||||||
// Code: ErrCodeVoteAlreadyCast,
|
|
||||||
// Message: "this vote has already been cast",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (api *Api) RegisterDeviceToken(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) RegisterDeviceToken(w http.ResponseWriter, r *http.Request) {
|
||||||
user := GetUser(r)
|
user := common.GetUser(r)
|
||||||
|
|
||||||
api.App.Logger.PrintInfo("register device token: request", map[string]string{
|
api.App.Logger.PrintInfo("register device token: request", map[string]string{
|
||||||
"user_id": fmt.Sprint(user.ID),
|
"user_id": fmt.Sprint(user.ID),
|
||||||
@ -16,7 +18,7 @@ func (api *Api) RegisterDeviceToken(w http.ResponseWriter, r *http.Request) {
|
|||||||
var input struct {
|
var input struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
if err := api.readJSON(w, r, &input); err != nil {
|
if err := common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.App.Logger.PrintInfo("register device token: bad request", map[string]string{
|
api.App.Logger.PrintInfo("register device token: bad request", map[string]string{
|
||||||
"user_id": fmt.Sprint(user.ID),
|
"user_id": fmt.Sprint(user.ID),
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
@ -47,12 +49,12 @@ func (api *Api) RegisterDeviceToken(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *Api) DeleteDeviceToken(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) DeleteDeviceToken(w http.ResponseWriter, r *http.Request) {
|
||||||
user := GetUser(r)
|
user := common.GetUser(r)
|
||||||
|
|
||||||
var input struct {
|
var input struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
if err := api.readJSON(w, r, &input); err != nil {
|
if err := common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.BadRequestResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
145
cmd/party/api/errors.go
Normal file
145
cmd/party/api/errors.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import(
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── Error responses ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type apiError struct {
|
||||||
|
Code common.ErrorCode `json:"code,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Details map[string]string `json:"details,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) LogError(r *http.Request, err error) {
|
||||||
|
api.App.Logger.PrintError(err, map[string]string{
|
||||||
|
"request_method": r.Method,
|
||||||
|
"request_url": r.URL.String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) errorResponse(w http.ResponseWriter, r *http.Request, status int, ae apiError) {
|
||||||
|
if err := common.WriteJSON(w, status, common.Envelope{"error": ae}, nil); err != nil {
|
||||||
|
api.App.LogError(r, err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) ServerErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
api.App.LogError(r, err)
|
||||||
|
api.errorResponse(w, r, http.StatusInternalServerError, apiError{
|
||||||
|
Message: "the server encountered a problem and could not process your request",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) NotFoundResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.errorResponse(w, r, http.StatusNotFound, apiError{
|
||||||
|
Message: "the requested resource could not be found",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) MethodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.errorResponse(w, r, http.StatusMethodNotAllowed, apiError{
|
||||||
|
Message: fmt.Sprintf("the %s method is not supported for this resource", r.Method),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) BadRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
api.errorResponse(w, r, http.StatusBadRequest, apiError{Message: err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) FailedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {
|
||||||
|
api.errorResponse(w, r, http.StatusUnprocessableEntity, apiError{
|
||||||
|
Code: common.ErrCodeValidationFailed,
|
||||||
|
Message: "validation failed",
|
||||||
|
Details: errors,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) EditConflictResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.errorResponse(w, r, http.StatusConflict, apiError{
|
||||||
|
Code: common.ErrCodeEditConflict,
|
||||||
|
Message: "unable to update the record due to an edit conflict, please try again",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) InvalidCredentialsResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.errorResponse(w, r, http.StatusUnauthorized, apiError{
|
||||||
|
Code: common.ErrCodeInvalidCredentials,
|
||||||
|
Message: "invalid authentication credentials",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) InvalidAuthenticationTokenResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("WWW-Authenticate", "Bearer")
|
||||||
|
api.errorResponse(w, r, http.StatusUnauthorized, apiError{
|
||||||
|
Code: common.ErrCodeInvalidAuthToken,
|
||||||
|
Message: "invalid or missing authentication token",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) RateLimitExceededResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.errorResponse(w, r, http.StatusTooManyRequests, apiError{
|
||||||
|
Message: "rate limit exceeded",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) AuthenticationRequiredResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.errorResponse(w, r, http.StatusUnauthorized, apiError{
|
||||||
|
Code: common.ErrCodeAuthRequired,
|
||||||
|
Message: "you must be authenticated to access this resource",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) InactiveAccountResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.errorResponse(w, r, http.StatusForbidden, apiError{
|
||||||
|
Code: common.ErrCodeInactiveAccount,
|
||||||
|
Message: "your user account must be activated to access this resource",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) NotPermittedResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.errorResponse(w, r, http.StatusForbidden, apiError{
|
||||||
|
Code: common.ErrCodeNotPermitted,
|
||||||
|
Message: "your user account doesn't have the necessary permissions to access this resource",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) AlreadyVotedResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.errorResponse(w, r, http.StatusConflict, apiError{
|
||||||
|
Code: common.ErrCodeAlreadyVoted,
|
||||||
|
Message: "your user account already voted for this issue",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) AlreadyBlindSignedResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.errorResponse(w, r, http.StatusConflict, apiError{
|
||||||
|
Code: common.ErrCodeAlreadyBlindSigned,
|
||||||
|
Message: "already requested a blind signature for this issue",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) BlindedVoteOutOfRangeResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.errorResponse(w, r, http.StatusUnprocessableEntity, apiError{
|
||||||
|
Code: common.ErrCodeBlindedVoteRange,
|
||||||
|
Message: "blinded_vote is out of valid range",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) InvalidSignatureResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.errorResponse(w, r, http.StatusUnprocessableEntity, apiError{
|
||||||
|
Code: common.ErrCodeInvalidSignature,
|
||||||
|
Message: "invalid signature",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Api) VoteAlreadyCastResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.errorResponse(w, r, http.StatusConflict, apiError{
|
||||||
|
Code: common.ErrCodeVoteAlreadyCast,
|
||||||
|
Message: "this vote has already been cast",
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,7 +1,9 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
import "party.at/party/cmd/party/common"
|
"net/http"
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
|
)
|
||||||
|
|
||||||
func (api *Api) Healthcheck(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) Healthcheck(w http.ResponseWriter, r *http.Request) {
|
||||||
env := common.Envelope{
|
env := common.Envelope{
|
||||||
|
|||||||
@ -31,13 +31,13 @@ func (api *Api) ListIssues(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
issues, metadata, err := api.App.FetchIssues(input.Title, input.Filters, GetUser(r))
|
issues, metadata, err := api.App.FetchIssues(input.Title, input.Filters, common.GetUser(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.writeJSON(w, http.StatusOK, envelope{"issues": issues, "metadata": metadata}, nil); err != nil {
|
if err = common.WriteJSON(w, http.StatusOK, common.Envelope{"issues": issues, "metadata": metadata}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,7 +51,7 @@ func (api *Api) CreateIssue(w http.ResponseWriter, r *http.Request) {
|
|||||||
Options []string `json:"options"`
|
Options []string `json:"options"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := api.readJSON(w, r, &input); err != nil {
|
if err := common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.BadRequestResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -69,7 +69,7 @@ func (api *Api) CreateIssue(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
headers := make(http.Header)
|
headers := make(http.Header)
|
||||||
headers.Set("Location", fmt.Sprintf("/v1/issues/%d", issue.ID))
|
headers.Set("Location", fmt.Sprintf("/v1/issues/%d", issue.ID))
|
||||||
if err = api.writeJSON(w, http.StatusCreated, envelope{"issue": issue, "options": options}, headers); err != nil {
|
if err = common.WriteJSON(w, http.StatusCreated, common.Envelope{"issue": issue, "options": options}, headers); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,7 +81,7 @@ func (api *Api) ReadIssue(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := api.App.GetIssue(id, GetUser(r))
|
result, err := api.App.GetIssue(id, common.GetUser(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, data.ErrRecordNotFound) {
|
if errors.Is(err, data.ErrRecordNotFound) {
|
||||||
api.NotFoundResponse(w, r)
|
api.NotFoundResponse(w, r)
|
||||||
@ -91,7 +91,7 @@ func (api *Api) ReadIssue(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.writeJSON(w, http.StatusOK, envelope{"issue": result.IssueDetail, "options": result.Options}, nil); err != nil {
|
if err = common.WriteJSON(w, http.StatusOK, common.Envelope{"issue": result.IssueDetail, "options": result.Options}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,7 +110,7 @@ func (api *Api) UpdateIssue(w http.ResponseWriter, r *http.Request) {
|
|||||||
EndTime *time.Time `json:"end_time"`
|
EndTime *time.Time `json:"end_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.readJSON(w, r, &input); err != nil {
|
if err = common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.BadRequestResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -131,7 +131,7 @@ func (api *Api) UpdateIssue(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.writeJSON(w, http.StatusOK, envelope{"issue": issue}, nil); err != nil {
|
if err = common.WriteJSON(w, http.StatusOK, common.Envelope{"issue": issue}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,7 +152,7 @@ func (api *Api) DeleteIssue(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.writeJSON(w, http.StatusOK, envelope{"message": "issue successfully deleted"}, nil); err != nil {
|
if err = common.WriteJSON(w, http.StatusOK, common.Envelope{"message": "issue successfully deleted"}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -174,13 +174,13 @@ func (api *Api) ReadIssuePubKey(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.writeJSON(w, http.StatusOK, envelope{"public_key": pubKey}, nil); err != nil {
|
if err = common.WriteJSON(w, http.StatusOK, common.Envelope{"public_key": pubKey}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *Api) BlindSignIssueVote(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) BlindSignIssueVote(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := api.readIDParam(r)
|
id, err := common.ReadIDParam(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.NotFoundResponse(w, r)
|
api.NotFoundResponse(w, r)
|
||||||
return
|
return
|
||||||
@ -189,12 +189,12 @@ func (api *Api) BlindSignIssueVote(w http.ResponseWriter, r *http.Request) {
|
|||||||
var input struct {
|
var input struct {
|
||||||
BlindedVote []byte `json:"blinded_vote"`
|
BlindedVote []byte `json:"blinded_vote"`
|
||||||
}
|
}
|
||||||
if err = api.readJSON(w, r, &input); err != nil {
|
if err = common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.BadRequestResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
signed, err := api.App.BlindSign(id, input.BlindedVote, GetUser(r))
|
signed, err := api.App.BlindSign(id, input.BlindedVote, common.GetUser(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, data.ErrRecordNotFound):
|
case errors.Is(err, data.ErrRecordNotFound):
|
||||||
@ -209,7 +209,7 @@ func (api *Api) BlindSignIssueVote(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.writeJSON(w, http.StatusOK, envelope{"signed": signed}, nil); err != nil {
|
if err = common.WriteJSON(w, http.StatusOK, common.Envelope{"signed": signed}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
import "fmt"
|
"net/http"
|
||||||
// import "party.at/party/cmd/party/common"
|
"fmt"
|
||||||
import "time"
|
"party.at/party/cmd/party/common"
|
||||||
import "sync"
|
"time"
|
||||||
import "golang.org/x/time/rate"
|
"sync"
|
||||||
import "net"
|
"golang.org/x/time/rate"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
func (api *Api) RecoverPanic(next http.Handler) http.Handler {
|
func (api *Api) RecoverPanic(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -22,7 +24,7 @@ func (api *Api) RecoverPanic(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
func (api *Api) RequireAuthenticatedUser(next http.HandlerFunc) http.HandlerFunc {
|
func (api *Api) RequireAuthenticatedUser(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if GetUser(r).IsAnonymous() {
|
if common.GetUser(r).IsAnonymous() {
|
||||||
api.AuthenticationRequiredResponse(w, r)
|
api.AuthenticationRequiredResponse(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -32,7 +34,7 @@ func (api *Api) RequireAuthenticatedUser(next http.HandlerFunc) http.HandlerFunc
|
|||||||
|
|
||||||
func (api *Api) RequireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
|
func (api *Api) RequireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
|
||||||
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if !GetUser(r).Activated {
|
if !common.GetUser(r).Activated {
|
||||||
api.InactiveAccountResponse(w, r)
|
api.InactiveAccountResponse(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -43,7 +45,7 @@ func (api *Api) RequireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
|
|
||||||
func (api *Api) RequirePermission(code string, next http.HandlerFunc) http.HandlerFunc {
|
func (api *Api) RequirePermission(code string, next http.HandlerFunc) http.HandlerFunc {
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
permissions, err := api.App.Models.Permissions.GetAllForUser(GetUser(r).ID)
|
permissions, err := api.App.Models.Permissions.GetAllForUser(common.GetUser(r).ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (api *Api) ListMPs(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) ListMPs(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -19,7 +21,7 @@ func (api *Api) ListMPs(w http.ResponseWriter, r *http.Request) {
|
|||||||
return members[i].LastName < members[j].LastName
|
return members[i].LastName < members[j].LastName
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := api.writeJSON(w, http.StatusOK, envelope{
|
if err := common.WriteJSON(w, http.StatusOK, common.Envelope{
|
||||||
"members": members,
|
"members": members,
|
||||||
"total": len(members),
|
"total": len(members),
|
||||||
}, nil); err != nil {
|
}, nil); err != nil {
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (api *Api) GetParlVoteDetail(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) GetParlVoteDetail(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -26,7 +28,7 @@ func (api *Api) GetParlVoteDetail(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := api.writeJSON(w, http.StatusOK, envelope{"vote": detail}, nil); err != nil {
|
if err := common.WriteJSON(w, http.StatusOK, common.Envelope{"vote": detail}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"party.at/party/cmd/party/parlament"
|
"party.at/party/cmd/party/parlament"
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type apiPartyStat struct {
|
type apiPartyStat struct {
|
||||||
@ -95,7 +96,7 @@ func (api *Api) ListParlVotes(w http.ResponseWriter, r *http.Request) {
|
|||||||
return partyStats[i].Code < partyStats[j].Code
|
return partyStats[i].Code < partyStats[j].Code
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := api.writeJSON(w, http.StatusOK, envelope{
|
if err := common.WriteJSON(w, http.StatusOK, common.Envelope{
|
||||||
"documents": docs,
|
"documents": docs,
|
||||||
"total": len(docs),
|
"total": len(docs),
|
||||||
"total_with_votes": totalWithVotes,
|
"total_with_votes": totalWithVotes,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"party.at/party/internal/data"
|
"party.at/party/internal/data"
|
||||||
"party.at/party/internal/validator"
|
"party.at/party/internal/validator"
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (api *Api) CreateAuthenticationToken(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) CreateAuthenticationToken(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -15,7 +16,7 @@ func (api *Api) CreateAuthenticationToken(w http.ResponseWriter, r *http.Request
|
|||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := api.readJSON(w, r, &input); err != nil {
|
if err := common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.BadRequestResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -39,7 +40,7 @@ func (api *Api) CreateAuthenticationToken(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.writeJSON(w, http.StatusCreated, envelope{"authentication_token": token}, nil); err != nil {
|
if err = common.WriteJSON(w, http.StatusCreated, common.Envelope{"authentication_token": token}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,7 +65,7 @@ func (api *Api) DeleteAuthenticationToken(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := api.writeJSON(w, http.StatusOK, envelope{"message": "authentication token successfully deleted"}, nil); err != nil {
|
if err := common.WriteJSON(w, http.StatusOK, common.Envelope{"message": "authentication token successfully deleted"}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,9 +19,9 @@ func (api *Api) ListUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
v := validator.New()
|
v := validator.New()
|
||||||
qs := r.URL.Query()
|
qs := r.URL.Query()
|
||||||
|
|
||||||
input.Filters.Page = api.readInt(qs, "page", 1, v)
|
input.Filters.Page = common.ReadInt(qs, "page", 1, v)
|
||||||
input.Filters.PageSize = api.readInt(qs, "page_size", 20, v)
|
input.Filters.PageSize = common.ReadInt(qs, "page_size", 20, v)
|
||||||
input.Filters.Sort = api.readString(qs, "sort", "id")
|
input.Filters.Sort = common.ReadString(qs, "sort", "id")
|
||||||
input.Filters.SortSafelist = []string{"id", "-id", "name", "-name", "email", "-email", "created", "-created"}
|
input.Filters.SortSafelist = []string{"id", "-id", "name", "-name", "email", "-email", "created", "-created"}
|
||||||
|
|
||||||
if data.ValidateFilters(v, input.Filters); !v.Valid() {
|
if data.ValidateFilters(v, input.Filters); !v.Valid() {
|
||||||
@ -35,7 +35,7 @@ func (api *Api) ListUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.writeJSON(w, http.StatusOK, envelope{"users": users, "metadata": metadata}, nil); err != nil {
|
if err = common.WriteJSON(w, http.StatusOK, common.Envelope{"users": users, "metadata": metadata}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,7 +54,7 @@ func (api *Api) CreateUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := api.readJSON(w, r, &input); err != nil {
|
if err := common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.BadRequestResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -90,7 +90,7 @@ func (api *Api) CreateUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.writeJSON(w, http.StatusCreated, envelope{"user": user, "authentication_token": authToken}, nil); err != nil {
|
if err = common.WriteJSON(w, http.StatusCreated, common.Envelope{"user": user, "authentication_token": authToken}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -100,7 +100,7 @@ func (api *Api) ReadUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
param := r.PathValue("id")
|
param := r.PathValue("id")
|
||||||
|
|
||||||
if param == "me" {
|
if param == "me" {
|
||||||
id = GetUser(r).ID
|
id = common.GetUser(r).ID
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
id, err = strconv.ParseInt(param, 10, 64)
|
id, err = strconv.ParseInt(param, 10, 64)
|
||||||
@ -110,7 +110,7 @@ func (api *Api) ReadUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if GetUser(r).ID != id {
|
if common.GetUser(r).ID != id {
|
||||||
api.errorResponse(w, r, http.StatusForbidden, apiError{
|
api.errorResponse(w, r, http.StatusForbidden, apiError{
|
||||||
Code: common.ErrCodeNotPermitted,
|
Code: common.ErrCodeNotPermitted,
|
||||||
Message: "your user account doesn't have the necessary permissions to access this resource",
|
Message: "your user account doesn't have the necessary permissions to access this resource",
|
||||||
@ -128,29 +128,29 @@ func (api *Api) ReadUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.writeJSON(w, http.StatusOK, envelope{"user": user}, nil); err != nil {
|
if err = common.WriteJSON(w, http.StatusOK, common.Envelope{"user": user}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *Api) ReadAuthenticatedUser(w http.ResponseWriter, r *http.Request) {
|
// func (api *Api) ReadAuthenticatedUser(w http.ResponseWriter, r *http.Request) {
|
||||||
user, err := api.App.GetUser(GetUser(r).ID)
|
// user := common.GetUser(r).ID
|
||||||
if err != nil {
|
// // if err != nil {
|
||||||
if errors.Is(err, data.ErrRecordNotFound) {
|
// // if errors.Is(err, data.ErrRecordNotFound) {
|
||||||
api.NotFoundResponse(w, r)
|
// // api.NotFoundResponse(w, r)
|
||||||
} else {
|
// // } else {
|
||||||
api.ServerErrorResponse(w, r, err)
|
// // api.ServerErrorResponse(w, r, err)
|
||||||
}
|
// // }
|
||||||
return
|
// // return
|
||||||
}
|
// // }
|
||||||
|
|
||||||
if err = api.writeJSON(w, http.StatusOK, envelope{"user": user}, nil); err != nil {
|
// if err := common.WriteJSON(w, http.StatusOK, common.Envelope{"user": user}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
// api.ServerErrorResponse(w, r, err)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (api *Api) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := api.readIDParam(r)
|
id, err := common.ReadIDParam(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.NotFoundResponse(w, r)
|
api.NotFoundResponse(w, r)
|
||||||
return
|
return
|
||||||
@ -165,7 +165,7 @@ func (api *Api) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.writeJSON(w, http.StatusOK, envelope{"message": "user successfully deleted"}, nil); err != nil {
|
if err = common.WriteJSON(w, http.StatusOK, common.Envelope{"message": "user successfully deleted"}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -175,7 +175,7 @@ func (api *Api) ActivateUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
TokenPlaintext string `json:"token"`
|
TokenPlaintext string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := api.readJSON(w, r, &input); err != nil {
|
if err := common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.BadRequestResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -200,7 +200,7 @@ func (api *Api) ActivateUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.writeJSON(w, http.StatusOK, envelope{"user": user}, nil); err != nil {
|
if err = common.WriteJSON(w, http.StatusOK, common.Envelope{"user": user}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"party.at/party/internal/data"
|
"party.at/party/internal/data"
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (api *Api) Vote(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) Vote(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -15,7 +16,7 @@ func (api *Api) Vote(w http.ResponseWriter, r *http.Request) {
|
|||||||
Signature []byte `json:"signature"`
|
Signature []byte `json:"signature"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := api.readJSON(w, r, &input); err != nil {
|
if err := common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.BadRequestResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -34,7 +35,7 @@ func (api *Api) Vote(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := api.writeJSON(w, http.StatusCreated, envelope{"message": "vote successfully cast"}, nil); err != nil {
|
if err := common.WriteJSON(w, http.StatusCreated, common.Envelope{"message": "vote successfully cast"}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
apns "github.com/sideshow/apns2"
|
apns "github.com/sideshow/apns2"
|
||||||
"party.at/party/cmd/party/parlament"
|
"party.at/party/cmd/party/parlament"
|
||||||
@ -81,3 +83,31 @@ func (app *Application) background(fn func()) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const userKey contextKey = "web_user"
|
||||||
|
const permissionsKey contextKey = "web_permissions"
|
||||||
|
|
||||||
|
func SetUser(r *http.Request, user *data.User) *http.Request {
|
||||||
|
return r.WithContext(context.WithValue(r.Context(), userKey, user))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUser(r *http.Request) *data.User {
|
||||||
|
user, ok := r.Context().Value(userKey).(*data.User)
|
||||||
|
if !ok {
|
||||||
|
return data.AnonymousUser
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetPermissions(r *http.Request, permissions data.Permissions) *http.Request {
|
||||||
|
return r.WithContext(context.WithValue(r.Context(), permissionsKey, permissions))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPermissions(r *http.Request) data.Permissions {
|
||||||
|
permissions, ok := r.Context().Value(permissionsKey).(data.Permissions)
|
||||||
|
if !ok {
|
||||||
|
return data.Permissions{}
|
||||||
|
}
|
||||||
|
return permissions
|
||||||
|
}
|
||||||
|
|||||||
@ -1,103 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"party.at/party/internal/validator"
|
|
||||||
)
|
|
||||||
|
|
||||||
type envelope map[string]interface{}
|
|
||||||
|
|
||||||
type apiError struct {
|
|
||||||
Code ErrorCode `json:"code,omitempty"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Details map[string]string `json:"details,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── JSON helpers ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func (app *Application) ReadJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
|
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, 1048576)
|
|
||||||
dec := json.NewDecoder(r.Body)
|
|
||||||
dec.DisallowUnknownFields()
|
|
||||||
|
|
||||||
err := dec.Decode(dst)
|
|
||||||
if err != nil {
|
|
||||||
var syntaxError *json.SyntaxError
|
|
||||||
var unmarshalTypeError *json.UnmarshalTypeError
|
|
||||||
var invalidUnmarshalError *json.InvalidUnmarshalError
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case errors.As(err, &syntaxError):
|
|
||||||
return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset)
|
|
||||||
case errors.Is(err, io.ErrUnexpectedEOF):
|
|
||||||
return errors.New("body contains badly-formed JSON")
|
|
||||||
case errors.As(err, &unmarshalTypeError):
|
|
||||||
if unmarshalTypeError.Field != "" {
|
|
||||||
return fmt.Errorf("body contains incorrect JSON type for field %q", unmarshalTypeError.Field)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("body contains incorrect JSON type (at character %d)", unmarshalTypeError.Offset)
|
|
||||||
case errors.Is(err, io.EOF):
|
|
||||||
return errors.New("body must not be empty")
|
|
||||||
case strings.HasPrefix(err.Error(), "json: unknown field "):
|
|
||||||
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
|
|
||||||
return fmt.Errorf("body contains unknown key %s", fieldName)
|
|
||||||
case err.Error() == "http: request body too large":
|
|
||||||
return fmt.Errorf("body must not be larger than 1048576 bytes")
|
|
||||||
case errors.As(err, &invalidUnmarshalError):
|
|
||||||
panic(err)
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = dec.Decode(&struct{}{})
|
|
||||||
if err != io.EOF {
|
|
||||||
return errors.New("body must only contain a single JSON value")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadIDParam(r *http.Request) (int64, error) {
|
|
||||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
||||||
if err != nil || id < 1 {
|
|
||||||
return 0, errors.New("invalid id parameter")
|
|
||||||
}
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadString(qs url.Values, key, defaultValue string) string {
|
|
||||||
if s := qs.Get(key); s != "" {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadInt(qs url.Values, key string, defaultValue int, v *validator.Validator) int {
|
|
||||||
s := qs.Get(key)
|
|
||||||
if s == "" {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
i, err := strconv.Atoi(s)
|
|
||||||
if err != nil {
|
|
||||||
v.AddError(key, "must be an integer value")
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Error responses ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func (app *Application) LogError(r *http.Request, err error) {
|
|
||||||
app.Logger.PrintError(err, map[string]string{
|
|
||||||
"request_method": r.Method,
|
|
||||||
"request_url": r.URL.String(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -19,44 +19,6 @@ import (
|
|||||||
|
|
||||||
type Envelope map[string]interface{}
|
type Envelope map[string]interface{}
|
||||||
|
|
||||||
func readString(qs url.Values, key string, defaultValue string) string {
|
|
||||||
s := qs.Get(key)
|
|
||||||
if s == "" {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func readCSV(qs url.Values, key string, defaultValue []string) []string {
|
|
||||||
csv := qs.Get(key)
|
|
||||||
if csv == "" {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return strings.Split(csv, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
func readInt(qs url.Values, key string, defaultValue int, v *validator.Validator) int {
|
|
||||||
s := qs.Get(key)
|
|
||||||
if s == "" {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
i, err := strconv.Atoi(s)
|
|
||||||
if err != nil {
|
|
||||||
v.AddError(key, "must be an integer value")
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
// func readIDParam(r *http.Request) (int64, error) {
|
|
||||||
// params := httprouter.ParamsFromContext(r.Context())
|
|
||||||
// id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
|
|
||||||
// if err != nil || id < 1 {
|
|
||||||
// return 0, errors.New("invalid id parameter")
|
|
||||||
// }
|
|
||||||
// return id, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func WriteJSON(w http.ResponseWriter, status int, data Envelope, headers http.Header) error {
|
func WriteJSON(w http.ResponseWriter, status int, data Envelope, headers http.Header) error {
|
||||||
js, err := json.MarshalIndent(data, "", "\t")
|
js, err := json.MarshalIndent(data, "", "\t")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -72,7 +34,7 @@ func WriteJSON(w http.ResponseWriter, status int, data Envelope, headers http.He
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
|
func ReadJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
|
||||||
maxBytes := 1048576
|
maxBytes := 1048576
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
|
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
|
||||||
dec := json.NewDecoder(r.Body)
|
dec := json.NewDecoder(r.Body)
|
||||||
@ -125,3 +87,48 @@ func GenerateIssueKey() ([]byte, int, []byte, error) {
|
|||||||
})
|
})
|
||||||
return key.N.Bytes(), key.E, privPEM, err
|
return key.N.Bytes(), key.E, privPEM, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReadIDParam(r *http.Request) (int64, error) {
|
||||||
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
|
if err != nil || id < 1 {
|
||||||
|
return 0, errors.New("invalid id parameter")
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadString(qs url.Values, key, defaultValue string) string {
|
||||||
|
if s := qs.Get(key); s != "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadInt(qs url.Values, key string, defaultValue int, v *validator.Validator) int {
|
||||||
|
s := qs.Get(key)
|
||||||
|
if s == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
i, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
v.AddError(key, "must be an integer value")
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadCSV(qs url.Values, key string, defaultValue []string) []string {
|
||||||
|
csv := qs.Get(key)
|
||||||
|
if csv == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return strings.Split(csv, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Error responses ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func (app *Application) LogError(r *http.Request, err error) {
|
||||||
|
app.Logger.PrintError(err, map[string]string{
|
||||||
|
"request_method": r.Method,
|
||||||
|
"request_url": r.URL.String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -54,8 +54,9 @@ func (app *Application) RegisterUser(input RegisterUserInput) (*data.User, *data
|
|||||||
|
|
||||||
role := "viewer"
|
role := "viewer"
|
||||||
if app.Config.Env == "development" {
|
if app.Config.Env == "development" {
|
||||||
role = "admin"
|
role = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := app.Models.Roles.AssignToUser(user.ID, role); err != nil {
|
if err := app.Models.Roles.AssignToUser(user.ID, role); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,87 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ws(w http.ResponseWriter, r *http.Request) {
|
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Upgrade error:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
case t := <-ticker.C:
|
|
||||||
msg := map[string]interface{}{
|
|
||||||
"type": "server_tick",
|
|
||||||
"timestamp": t.Format(time.RFC3339),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := conn.WriteJSON(msg); err != nil {
|
|
||||||
fmt.Println("Write error:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
var msg Message
|
|
||||||
err := conn.ReadJSON(&msg)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Read error:", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fmt.Println(msg)
|
|
||||||
|
|
||||||
// Send a response
|
|
||||||
// response := fmt.Sprintf("Server time: %s", time.Now())
|
|
||||||
// if err := conn.WriteMessage(websocket.TextMessage, []byte(response)); err != nil {
|
|
||||||
// fmt.Println("Write error:", err)
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// func handleMobileLogin(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// // 1. Get the token from the request header
|
|
||||||
// rawIDToken := r.Header.Get("Authorization")
|
|
||||||
|
|
||||||
// // 2. Initialize the verifier (pointing to ID Austria's keys)
|
|
||||||
// verifier := provider.Verifier(&oidc.Config{ClientID: "YOUR_APP_ID"})
|
|
||||||
|
|
||||||
// // 3. Verify the signature and expiration
|
|
||||||
// idToken, err := verifier.Verify(ctx, rawIDToken)
|
|
||||||
// if err != nil {
|
|
||||||
// http.Error(w, "Invalid Token", http.StatusUnauthorized)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 4. Extract User Data
|
|
||||||
// var claims struct {
|
|
||||||
// Subject string `json:"sub"` // This is the unique ID
|
|
||||||
// Name string `json:"name"`
|
|
||||||
// }
|
|
||||||
// if err := idToken.Claims(&claims); err != nil {
|
|
||||||
// // Handle error
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 5. Create your own application session for the mobile app
|
|
||||||
// issueLocalSession(w, claims.Subject)
|
|
||||||
// }
|
|
||||||
|
|
||||||
func redirectToHTTPS(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Redirect(w, r, "https://localhost:8443"+r.URL.RequestURI(), http.StatusMovedPermanently)
|
|
||||||
}
|
|
||||||
@ -16,12 +16,8 @@ func routes(app *common.Application) http.Handler {
|
|||||||
}
|
}
|
||||||
apiMux := http.NewServeMux()
|
apiMux := http.NewServeMux()
|
||||||
|
|
||||||
// Note: Standard library doesn't have a simple .NotFound property.
|
|
||||||
// You handle that by wrapping the final mux in middleware or a custom handler.
|
|
||||||
|
|
||||||
apiMux.HandleFunc("GET /v1/healthcheck", api.Healthcheck)
|
apiMux.HandleFunc("GET /v1/healthcheck", api.Healthcheck)
|
||||||
|
|
||||||
// Path parameters use {id} instead of :id
|
|
||||||
apiMux.HandleFunc("GET /v1/issues", api.RequirePermission("issues:read", api.ListIssues))
|
apiMux.HandleFunc("GET /v1/issues", api.RequirePermission("issues:read", api.ListIssues))
|
||||||
apiMux.HandleFunc("POST /v1/issues", api.RequirePermission("issues:write", api.CreateIssue))
|
apiMux.HandleFunc("POST /v1/issues", api.RequirePermission("issues:write", api.CreateIssue))
|
||||||
apiMux.HandleFunc("GET /v1/issues/{id}", api.RequirePermission("issues:read", api.ReadIssue))
|
apiMux.HandleFunc("GET /v1/issues/{id}", api.RequirePermission("issues:read", api.ReadIssue))
|
||||||
@ -29,24 +25,18 @@ func routes(app *common.Application) http.Handler {
|
|||||||
apiMux.HandleFunc("DELETE /v1/issues/{id}", api.RequirePermission("issues:write", api.DeleteIssue))
|
apiMux.HandleFunc("DELETE /v1/issues/{id}", api.RequirePermission("issues:write", api.DeleteIssue))
|
||||||
apiMux.HandleFunc("GET /v1/issues/{id}/pubkey", api.RequirePermission("issues:read", api.ReadIssuePubKey))
|
apiMux.HandleFunc("GET /v1/issues/{id}/pubkey", api.RequirePermission("issues:read", api.ReadIssuePubKey))
|
||||||
apiMux.HandleFunc("POST /v1/issues/{id}/blind-sign", api.RequirePermission("issues:read", api.BlindSignIssueVote))
|
apiMux.HandleFunc("POST /v1/issues/{id}/blind-sign", api.RequirePermission("issues:read", api.BlindSignIssueVote))
|
||||||
|
|
||||||
apiMux.HandleFunc("POST /v1/votes", api.Vote)
|
apiMux.HandleFunc("POST /v1/votes", api.Vote)
|
||||||
apiMux.HandleFunc("POST /v1/users", api.CreateUser)
|
apiMux.HandleFunc("POST /v1/users", api.CreateUser)
|
||||||
apiMux.HandleFunc("GET /v1/users", api.RequirePermission("users:read", api.ListUsers))
|
apiMux.HandleFunc("GET /v1/users", api.RequirePermission("users:read", api.ListUsers))
|
||||||
apiMux.HandleFunc("GET /v1/users/{id}", api.RequireAuthenticatedUser(api.ReadUser))
|
apiMux.HandleFunc("GET /v1/users/{id}", api.RequireAuthenticatedUser(api.ReadUser))
|
||||||
apiMux.HandleFunc("DELETE /v1/users/{id}", api.DeleteUser)
|
apiMux.HandleFunc("DELETE /v1/users/{id}", api.RequirePermission("users:write", api.DeleteUser))
|
||||||
apiMux.HandleFunc("PUT /v1/users/activated", api.ActivateUser)
|
apiMux.HandleFunc("PUT /v1/users/activated", api.ActivateUser)
|
||||||
|
|
||||||
apiMux.HandleFunc("POST /v1/tokens/authentication", api.CreateAuthenticationToken)
|
apiMux.HandleFunc("POST /v1/tokens/authentication", api.CreateAuthenticationToken)
|
||||||
apiMux.HandleFunc("DELETE /v1/tokens/authentication", api.RequireAuthenticatedUser(api.DeleteAuthenticationToken))
|
apiMux.HandleFunc("DELETE /v1/tokens/authentication", api.RequireAuthenticatedUser(api.DeleteAuthenticationToken))
|
||||||
|
|
||||||
apiMux.HandleFunc("POST /v1/device-tokens", api.RequireAuthenticatedUser(api.RegisterDeviceToken))
|
apiMux.HandleFunc("POST /v1/device-tokens", api.RequireAuthenticatedUser(api.RegisterDeviceToken))
|
||||||
apiMux.HandleFunc("DELETE /v1/device-tokens", api.RequireAuthenticatedUser(api.DeleteDeviceToken))
|
apiMux.HandleFunc("DELETE /v1/device-tokens", api.RequireAuthenticatedUser(api.DeleteDeviceToken))
|
||||||
|
|
||||||
apiMux.HandleFunc("GET /v1/mps", api.ListMPs)
|
apiMux.HandleFunc("GET /v1/mps", api.ListMPs)
|
||||||
apiMux.HandleFunc("GET /v1/parlament/votes", api.ListParlVotes)
|
apiMux.HandleFunc("GET /v1/parlament/votes", api.ListParlVotes)
|
||||||
|
|
||||||
// Wildcards use {name...}
|
|
||||||
apiMux.HandleFunc("GET /v1/parlament/votes/{path...}", api.GetParlVoteDetail)
|
apiMux.HandleFunc("GET /v1/parlament/votes/{path...}", api.GetParlVoteDetail)
|
||||||
apiMux.Handle("GET /debug/vars", expvar.Handler())
|
apiMux.Handle("GET /debug/vars", expvar.Handler())
|
||||||
|
|
||||||
@ -61,28 +51,30 @@ func routes(app *common.Application) http.Handler {
|
|||||||
webMux.HandleFunc("GET /", web.Home)
|
webMux.HandleFunc("GET /", web.Home)
|
||||||
webMux.HandleFunc("GET /register", web.Register)
|
webMux.HandleFunc("GET /register", web.Register)
|
||||||
webMux.HandleFunc("POST /register", web.RegisterUserPage)
|
webMux.HandleFunc("POST /register", web.RegisterUserPage)
|
||||||
webMux.HandleFunc("GET /issues", web.IssuesPage)
|
webMux.HandleFunc("GET /issues", web.RequirePermission("issues:read", web.IssuesPage))
|
||||||
webMux.HandleFunc("POST /issues", web.CreateIssueAction)
|
webMux.HandleFunc("POST /issues", web.RequirePermission("issues:write", web.CreateIssueAction))
|
||||||
webMux.HandleFunc("GET /issues/{id}", web.IssuePage)
|
webMux.HandleFunc("GET /issues/{id}", web.RequirePermission("issues:read", web.IssuePage))
|
||||||
webMux.HandleFunc("PATCH /issues/{id}", web.UpdateIssueAction)
|
webMux.HandleFunc("PATCH /issues/{id}", web.RequirePermission("issues:write", web.UpdateIssueAction))
|
||||||
webMux.HandleFunc("DELETE /issues/{id}", web.DeleteIssueAction)
|
webMux.HandleFunc("DELETE /issues/{id}", web.RequirePermission("issues:write", web.DeleteIssueAction))
|
||||||
webMux.HandleFunc("GET /issues/{id}/pubkey", web.GetIssuePubKey)
|
webMux.HandleFunc("GET /issues/{id}/pubkey", web.RequirePermission("issues:read", web.GetIssuePubKey))
|
||||||
webMux.HandleFunc("POST /issues/{id}/blind-sign", web.BlindSignIssue)
|
webMux.HandleFunc("POST /issues/{id}/blind-sign", web.RequirePermission("issues:read", web.BlindSignIssueVote))
|
||||||
webMux.HandleFunc("POST /votes", web.VoteAction)
|
webMux.HandleFunc("POST /votes", web.VoteAction)
|
||||||
webMux.HandleFunc("GET /users", web.UsersPage)
|
|
||||||
webMux.HandleFunc("GET /users/me", web.ProfilePage)
|
webMux.HandleFunc("GET /users", web.RequirePermission("users:read", web.UsersPage))
|
||||||
|
webMux.HandleFunc("GET /users/me", web.RequireAuthenticatedUser(web.ProfilePage))
|
||||||
webMux.HandleFunc("GET /users/activated", web.ActivatePage)
|
webMux.HandleFunc("GET /users/activated", web.ActivatePage)
|
||||||
webMux.HandleFunc("POST /users/activated", web.ActivateUserAction)
|
webMux.HandleFunc("POST /users/activated", web.ActivateUserAction)
|
||||||
webMux.HandleFunc("DELETE /users/{id}", web.DeleteUserAction)
|
webMux.HandleFunc("DELETE /users/{id}", web.RequirePermission("users:write", web.DeleteUserAction))
|
||||||
|
|
||||||
webMux.HandleFunc("GET /mps", web.MembersOfParliamentPage)
|
webMux.HandleFunc("GET /mps", web.MembersOfParliamentPage)
|
||||||
webMux.HandleFunc("GET /parlament", web.ParlVotesPage)
|
webMux.HandleFunc("GET /parlament", web.ParlVotesPage)
|
||||||
webMux.HandleFunc("GET /parlament/{path...}", web.ParlamentDispatch)
|
webMux.HandleFunc("GET /parlament/{path...}", web.ParlamentDispatch)
|
||||||
|
|
||||||
webMux.HandleFunc("POST /login", web.Login)
|
webMux.HandleFunc("POST /login", web.Login)
|
||||||
webMux.HandleFunc("POST /dev-login", web.DevLogin)
|
webMux.HandleFunc("POST /dev-login", web.DevLogin)
|
||||||
webMux.HandleFunc("POST /logout", web.Logout)
|
webMux.HandleFunc("POST /logout", web.Logout)
|
||||||
webMux.HandleFunc("GET /ws", ws)
|
webMux.HandleFunc("GET /ws", ws)
|
||||||
|
|
||||||
// Static files in standard library
|
|
||||||
fileServer := http.FileServer(http.Dir("web/static"))
|
fileServer := http.FileServer(http.Dir("web/static"))
|
||||||
webMux.Handle("GET /static/", http.StripPrefix("/static/", fileServer))
|
webMux.Handle("GET /static/", http.StripPrefix("/static/", fileServer))
|
||||||
|
|
||||||
|
|||||||
153
cmd/party/web/errors.go
Normal file
153
cmd/party/web/errors.go
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import(
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"party.at/party/internal/data"
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── Error responses ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type webError struct {
|
||||||
|
Code common.ErrorCode `json:"code,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Details map[string]string `json:"details,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) LogError(r *http.Request, err error) {
|
||||||
|
web.App.Logger.PrintError(err, map[string]string{
|
||||||
|
"request_method": r.Method,
|
||||||
|
"request_url": r.URL.String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) errorResponse(w http.ResponseWriter, r *http.Request, status int, we webError) {
|
||||||
|
// if err := common.WriteJSON(w, status, common.Envelope{"error": we}, nil); err != nil {
|
||||||
|
// web.LogError(r, err)
|
||||||
|
// w.WriteHeader(500)
|
||||||
|
// }
|
||||||
|
|
||||||
|
user := common.GetUser(r)
|
||||||
|
|
||||||
|
web.render(w, r, http.StatusOK, "error", struct {
|
||||||
|
AuthenticatedUser *data.User
|
||||||
|
FormErrors []string
|
||||||
|
IsDevelopment bool
|
||||||
|
Status int
|
||||||
|
We webError
|
||||||
|
}{
|
||||||
|
AuthenticatedUser: user,
|
||||||
|
IsDevelopment: web.App.Config.Env == "development",
|
||||||
|
Status: status,
|
||||||
|
We: we,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) ServerErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
web.LogError(r, err)
|
||||||
|
web.errorResponse(w, r, http.StatusInternalServerError, webError{
|
||||||
|
Message: "the server encountered a problem and could not process your request",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) NotFoundResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
web.errorResponse(w, r, http.StatusNotFound, webError{
|
||||||
|
Message: "the requested resource could not be found",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) MethodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
web.errorResponse(w, r, http.StatusMethodNotAllowed, webError{
|
||||||
|
Message: fmt.Sprintf("the %s method is not supported for this resource", r.Method),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) BadRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
web.errorResponse(w, r, http.StatusBadRequest, webError{Message: err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) FailedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {
|
||||||
|
web.errorResponse(w, r, http.StatusUnprocessableEntity, webError{
|
||||||
|
Code: common.ErrCodeValidationFailed,
|
||||||
|
Message: "validation failed",
|
||||||
|
Details: errors,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) EditConflictResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
web.errorResponse(w, r, http.StatusConflict, webError{
|
||||||
|
Code: common.ErrCodeEditConflict,
|
||||||
|
Message: "unable to update the record due to an edit conflict, please try again",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) InvalidCredentialsResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
web.errorResponse(w, r, http.StatusUnauthorized, webError{
|
||||||
|
Code: common.ErrCodeInvalidCredentials,
|
||||||
|
Message: "invalid authentication credentials",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) RateLimitExceededResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
web.errorResponse(w, r, http.StatusTooManyRequests, webError{
|
||||||
|
Message: "rate limit exceeded",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) AuthenticationRequiredResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
web.errorResponse(w, r, http.StatusUnauthorized, webError{
|
||||||
|
Code: common.ErrCodeAuthRequired,
|
||||||
|
Message: "you must be authenticated to access this resource",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) InactiveAccountResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
web.errorResponse(w, r, http.StatusForbidden, webError{
|
||||||
|
Code: common.ErrCodeInactiveAccount,
|
||||||
|
Message: "your user account must be activated to access this resource",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) NotPermittedResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
web.errorResponse(w, r, http.StatusForbidden, webError{
|
||||||
|
Code: common.ErrCodeNotPermitted,
|
||||||
|
Message: "your user account doesn't have the necessary permissions to access this resource",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) AlreadyVotedResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
web.errorResponse(w, r, http.StatusConflict, webError{
|
||||||
|
Code: common.ErrCodeAlreadyVoted,
|
||||||
|
Message: "your user account already voted for this issue",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) AlreadyBlindSignedResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
web.errorResponse(w, r, http.StatusConflict, webError{
|
||||||
|
Code: common.ErrCodeAlreadyBlindSigned,
|
||||||
|
Message: "already requested a blind signature for this issue",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) BlindedVoteOutOfRangeResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
web.errorResponse(w, r, http.StatusUnprocessableEntity, webError{
|
||||||
|
Code: common.ErrCodeBlindedVoteRange,
|
||||||
|
Message: "blinded_vote is out of valid range",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) InvalidSignatureResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
web.errorResponse(w, r, http.StatusUnprocessableEntity, webError{
|
||||||
|
Code: common.ErrCodeInvalidSignature,
|
||||||
|
Message: "invalid signature",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) VoteAlreadyCastResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
web.errorResponse(w, r, http.StatusConflict, webError{
|
||||||
|
Code: common.ErrCodeVoteAlreadyCast,
|
||||||
|
Message: "this vote has already been cast",
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -13,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (web *Web) Home(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) Home(w http.ResponseWriter, r *http.Request) {
|
||||||
if !getUser(r).IsAnonymous() {
|
if !common.GetUser(r).IsAnonymous() {
|
||||||
http.Redirect(w, r, "/issues", http.StatusSeeOther)
|
http.Redirect(w, r, "/issues", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (web *Web) IssuesPage(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) IssuesPage(w http.ResponseWriter, r *http.Request) {
|
||||||
user := getUser(r)
|
user := common.GetUser(r)
|
||||||
if user.IsAnonymous() {
|
if user.IsAnonymous() {
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
@ -30,21 +30,17 @@ func (web *Web) IssuesPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
permissions, _ := web.App.Models.Permissions.GetAllForUser(user.ID)
|
|
||||||
|
|
||||||
web.render(w, r, http.StatusOK, "issues", struct {
|
web.render(w, r, http.StatusOK, "issues", struct {
|
||||||
AuthenticatedUser *data.User
|
AuthenticatedUser *data.User
|
||||||
Issues []common.IssueDetail
|
Issues []common.IssueDetail
|
||||||
CanWriteIssues bool
|
|
||||||
}{
|
}{
|
||||||
AuthenticatedUser: user,
|
AuthenticatedUser: user,
|
||||||
Issues: issues,
|
Issues: issues,
|
||||||
CanWriteIssues: permissions.Include("issues:write"),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) CreateIssueAction(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) CreateIssueAction(w http.ResponseWriter, r *http.Request) {
|
||||||
user := getUser(r)
|
user := common.GetUser(r)
|
||||||
if user.IsAnonymous() {
|
if user.IsAnonymous() {
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
@ -88,7 +84,7 @@ func (web *Web) CreateIssueAction(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) IssuePage(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) IssuePage(w http.ResponseWriter, r *http.Request) {
|
||||||
user := getUser(r)
|
user := common.GetUser(r)
|
||||||
if user.IsAnonymous() {
|
if user.IsAnonymous() {
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
@ -111,21 +107,17 @@ func (web *Web) IssuePage(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
permissions, _ := web.App.Models.Permissions.GetAllForUser(user.ID)
|
|
||||||
|
|
||||||
web.render(w, r, http.StatusOK, "issue", struct {
|
web.render(w, r, http.StatusOK, "issue", struct {
|
||||||
AuthenticatedUser *data.User
|
AuthenticatedUser *data.User
|
||||||
Issue *common.IssueWithOptions
|
Issue *common.IssueWithOptions
|
||||||
CanWriteIssues bool
|
|
||||||
}{
|
}{
|
||||||
AuthenticatedUser: user,
|
AuthenticatedUser: user,
|
||||||
Issue: result,
|
Issue: result,
|
||||||
CanWriteIssues: permissions.Include("issues:write"),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) UpdateIssueAction(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) UpdateIssueAction(w http.ResponseWriter, r *http.Request) {
|
||||||
if getUser(r).IsAnonymous() {
|
if common.GetUser(r).IsAnonymous() {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -172,7 +164,7 @@ func (web *Web) UpdateIssueAction(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) DeleteIssueAction(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) DeleteIssueAction(w http.ResponseWriter, r *http.Request) {
|
||||||
if getUser(r).IsAnonymous() {
|
if common.GetUser(r).IsAnonymous() {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -220,8 +212,8 @@ func (web *Web) GetIssuePubKey(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) BlindSignIssue(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) BlindSignIssueVote(w http.ResponseWriter, r *http.Request) {
|
||||||
user := getUser(r)
|
user := common.GetUser(r)
|
||||||
if user.IsAnonymous() {
|
if user.IsAnonymous() {
|
||||||
common.WriteJSON(w, http.StatusUnauthorized, common.Envelope{"error": map[string]string{"message": "authentication required"}}, nil)
|
common.WriteJSON(w, http.StatusUnauthorized, common.Envelope{"error": map[string]string{"message": "authentication required"}}, nil)
|
||||||
return
|
return
|
||||||
@ -236,7 +228,7 @@ func (web *Web) BlindSignIssue(w http.ResponseWriter, r *http.Request) {
|
|||||||
var input struct {
|
var input struct {
|
||||||
BlindedVote []byte `json:"blinded_vote"`
|
BlindedVote []byte `json:"blinded_vote"`
|
||||||
}
|
}
|
||||||
if err = web.App.ReadJSON(w, r, &input); err != nil {
|
if err = common.ReadJSON(w, r, &input); err != nil {
|
||||||
common.WriteJSON(w, http.StatusBadRequest, common.Envelope{"error": map[string]string{"message": err.Error()}}, nil)
|
common.WriteJSON(w, http.StatusBadRequest, common.Envelope{"error": map[string]string{"message": err.Error()}}, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
"party.at/party/internal/data"
|
"party.at/party/internal/data"
|
||||||
"party.at/party/internal/validator"
|
"party.at/party/internal/validator"
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
const cookieName = "session"
|
const cookieName = "session"
|
||||||
@ -19,7 +20,7 @@ func (web *Web) AuthenticateCookie(next http.Handler) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
cookie, err := r.Cookie(cookieName)
|
cookie, err := r.Cookie(cookieName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r = setUser(r, data.AnonymousUser)
|
r = common.SetUser(r, data.AnonymousUser)
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -27,7 +28,7 @@ func (web *Web) AuthenticateCookie(next http.Handler) http.Handler {
|
|||||||
v := validator.New()
|
v := validator.New()
|
||||||
if data.ValidateTokenPlaintext(v, cookie.Value); !v.Valid() {
|
if data.ValidateTokenPlaintext(v, cookie.Value); !v.Valid() {
|
||||||
clearCookie(w)
|
clearCookie(w)
|
||||||
r = setUser(r, data.AnonymousUser)
|
r = common.SetUser(r, data.AnonymousUser)
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -40,7 +41,7 @@ func (web *Web) AuthenticateCookie(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
clearCookie(w)
|
clearCookie(w)
|
||||||
r = setUser(r, data.AnonymousUser)
|
r = common.SetUser(r, data.AnonymousUser)
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -53,38 +54,47 @@ func (web *Web) AuthenticateCookie(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
clearCookie(w)
|
clearCookie(w)
|
||||||
r = setUser(r, data.AnonymousUser)
|
r = common.SetUser(r, data.AnonymousUser)
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
r = setUser(r, user)
|
r = common.SetUser(r, user)
|
||||||
permissions, _ := web.App.Models.Permissions.GetAllForUser(user.ID)
|
permissions, _ := web.App.Models.Permissions.GetAllForUser(user.ID)
|
||||||
r = setPermissions(r, permissions)
|
r = common.SetPermissions(r, permissions)
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func setCookie(w http.ResponseWriter, token string, expiry time.Time) {
|
func (web *Web) RequireAuthenticatedUser(next http.HandlerFunc) http.HandlerFunc {
|
||||||
http.SetCookie(w, &http.Cookie{
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
Name: cookieName,
|
if common.GetUser(r).IsAnonymous() {
|
||||||
Value: token,
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
Expires: expiry,
|
return
|
||||||
Path: "/",
|
}
|
||||||
HttpOnly: true,
|
next.ServeHTTP(w, r)
|
||||||
SameSite: http.SameSiteLaxMode,
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) RequireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return web.RequireAuthenticatedUser(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user := common.GetUser(r)
|
||||||
|
|
||||||
|
if !user.Activated {
|
||||||
|
web.render(w, r, http.StatusOK, "forbidden", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearCookie(w http.ResponseWriter) {
|
func (web *Web) RequirePermission(code string, next http.HandlerFunc) http.HandlerFunc {
|
||||||
http.SetCookie(w, &http.Cookie{
|
return web.RequireActivatedUser(func(w http.ResponseWriter, r *http.Request) {
|
||||||
Name: cookieName,
|
if !common.GetPermissions(r).Include(code) {
|
||||||
Value: "",
|
web.NotPermittedResponse(w, r)
|
||||||
Expires: time.Unix(0, 0),
|
return
|
||||||
Path: "/",
|
}
|
||||||
HttpOnly: true,
|
next.ServeHTTP(w, r)
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
MaxAge: -1,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"party.at/party/cmd/party/parlament"
|
"party.at/party/cmd/party/parlament"
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
"party.at/party/internal/data"
|
"party.at/party/internal/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ func (web *Web) MembersOfParliamentPage(w http.ResponseWriter, r *http.Request)
|
|||||||
Groups []factionGroup
|
Groups []factionGroup
|
||||||
Total int
|
Total int
|
||||||
}{
|
}{
|
||||||
AuthenticatedUser: getUser(r),
|
AuthenticatedUser: common.GetUser(r),
|
||||||
Groups: groups,
|
Groups: groups,
|
||||||
Total: len(members),
|
Total: len(members),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -4,11 +4,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"party.at/party/cmd/party/parlament"
|
"party.at/party/cmd/party/parlament"
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
"party.at/party/internal/data"
|
"party.at/party/internal/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (web *Web) ParlamentDispatch(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) ParlamentDispatch(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.PathValue("path") == "/rows" {
|
p := r.PathValue("path")
|
||||||
|
if p == "rows" {
|
||||||
web.ParlVotesRowsFragment(w, r)
|
web.ParlVotesRowsFragment(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -21,7 +23,7 @@ func (web *Web) ParlVoteDetailPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
path := "/gegenstand" + routePath
|
path := "/gegenstand/" + routePath
|
||||||
|
|
||||||
detail, err := web.App.Parlament.GetDocumentVote(path)
|
detail, err := web.App.Parlament.GetDocumentVote(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -39,7 +41,7 @@ func (web *Web) ParlVoteDetailPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
Detail *parlament.DocumentVoteDetail
|
Detail *parlament.DocumentVoteDetail
|
||||||
Path string
|
Path string
|
||||||
}{
|
}{
|
||||||
AuthenticatedUser: getUser(r),
|
AuthenticatedUser: common.GetUser(r),
|
||||||
Detail: detail,
|
Detail: detail,
|
||||||
Path: path,
|
Path: path,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"party.at/party/cmd/party/parlament"
|
"party.at/party/cmd/party/parlament"
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
"party.at/party/internal/data"
|
"party.at/party/internal/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -121,7 +122,7 @@ func (web *Web) ParlVotesPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
AuthenticatedUser *data.User
|
AuthenticatedUser *data.User
|
||||||
Period string
|
Period string
|
||||||
}{
|
}{
|
||||||
AuthenticatedUser: getUser(r),
|
AuthenticatedUser: common.GetUser(r),
|
||||||
Period: parlament.CurrentPeriod,
|
Period: parlament.CurrentPeriod,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (web *Web) renderFragment(w http.ResponseWriter, r *http.Request, status int, name string, data interface{}) {
|
func (web *Web) renderFragment(w http.ResponseWriter, r *http.Request, status int, name string, data interface{}) {
|
||||||
funcs := template.FuncMap{
|
funcs := template.FuncMap{
|
||||||
"hasPermission": func(code string) bool {
|
"hasPermission": func(code string) bool {
|
||||||
return getPermissions(r).Include(code)
|
return common.GetPermissions(r).Include(code)
|
||||||
},
|
},
|
||||||
"partyColor": func(code string) string {
|
"partyColor": func(code string) string {
|
||||||
switch code {
|
switch code {
|
||||||
@ -54,7 +56,7 @@ func (web *Web) render(w http.ResponseWriter, r *http.Request, status int, page
|
|||||||
return m, nil
|
return m, nil
|
||||||
},
|
},
|
||||||
"hasPermission": func(code string) bool {
|
"hasPermission": func(code string) bool {
|
||||||
return getPermissions(r).Include(code)
|
return common.GetPermissions(r).Include(code)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ts, err := template.New("base").Funcs(funcs).ParseFiles(
|
ts, err := template.New("base").Funcs(funcs).ParseFiles(
|
||||||
|
|||||||
@ -14,7 +14,7 @@ func (web *Web) Register(w http.ResponseWriter, r *http.Request) {
|
|||||||
AuthenticatedUser *data.User
|
AuthenticatedUser *data.User
|
||||||
FormErrors []string
|
FormErrors []string
|
||||||
}{
|
}{
|
||||||
AuthenticatedUser: getUser(r),
|
AuthenticatedUser: common.GetUser(r),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ func (web *Web) RegisterUserPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
AuthenticatedUser *data.User
|
AuthenticatedUser *data.User
|
||||||
FormErrors []string
|
FormErrors []string
|
||||||
}{
|
}{
|
||||||
AuthenticatedUser: getUser(r),
|
AuthenticatedUser: common.GetUser(r),
|
||||||
FormErrors: formErrors,
|
FormErrors: formErrors,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@ -77,7 +77,7 @@ func (web *Web) RegisterUserPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) ProfilePage(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) ProfilePage(w http.ResponseWriter, r *http.Request) {
|
||||||
user := getUser(r)
|
user := common.GetUser(r)
|
||||||
if user.IsAnonymous() {
|
if user.IsAnonymous() {
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
@ -104,7 +104,7 @@ func (web *Web) ProfilePage(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) UsersPage(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) UsersPage(w http.ResponseWriter, r *http.Request) {
|
||||||
user := getUser(r)
|
user := common.GetUser(r)
|
||||||
if user.IsAnonymous() {
|
if user.IsAnonymous() {
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
@ -122,7 +122,7 @@ func (web *Web) UsersPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
permissions := getPermissions(r)
|
permissions := common.GetPermissions(r)
|
||||||
if !permissions.Include("users:read") {
|
if !permissions.Include("users:read") {
|
||||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
@ -145,7 +145,7 @@ func (web *Web) ActivatePage(w http.ResponseWriter, r *http.Request) {
|
|||||||
FormErrors []string
|
FormErrors []string
|
||||||
Token string
|
Token string
|
||||||
}{
|
}{
|
||||||
AuthenticatedUser: getUser(r),
|
AuthenticatedUser: common.GetUser(r),
|
||||||
Token: r.URL.Query().Get("token"),
|
Token: r.URL.Query().Get("token"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -171,7 +171,7 @@ func (web *Web) ActivateUserAction(w http.ResponseWriter, r *http.Request) {
|
|||||||
FormErrors []string
|
FormErrors []string
|
||||||
Token string
|
Token string
|
||||||
}{
|
}{
|
||||||
AuthenticatedUser: getUser(r),
|
AuthenticatedUser: common.GetUser(r),
|
||||||
FormErrors: []string{msg},
|
FormErrors: []string{msg},
|
||||||
Token: token,
|
Token: token,
|
||||||
})
|
})
|
||||||
@ -182,7 +182,7 @@ func (web *Web) ActivateUserAction(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) DeleteUserAction(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) DeleteUserAction(w http.ResponseWriter, r *http.Request) {
|
||||||
currentUser := getUser(r)
|
currentUser := common.GetUser(r)
|
||||||
if currentUser.IsAnonymous() {
|
if currentUser.IsAnonymous() {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -16,7 +16,7 @@ func (web *Web) VoteAction(w http.ResponseWriter, r *http.Request) {
|
|||||||
Signature []byte `json:"signature"`
|
Signature []byte `json:"signature"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := web.App.ReadJSON(w, r, &input); err != nil {
|
if err := common.ReadJSON(w, r, &input); err != nil {
|
||||||
common.WriteJSON(w, http.StatusBadRequest, common.Envelope{"error": map[string]string{"message": err.Error()}}, nil)
|
common.WriteJSON(w, http.StatusBadRequest, common.Envelope{"error": map[string]string{"message": err.Error()}}, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,42 +1,37 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
// "context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"party.at/party/cmd/party/common"
|
"party.at/party/cmd/party/common"
|
||||||
"party.at/party/internal/data"
|
// "party.at/party/internal/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Web struct {
|
type Web struct {
|
||||||
App *common.Application
|
App *common.Application
|
||||||
}
|
}
|
||||||
|
|
||||||
type contextKey string
|
func setCookie(w http.ResponseWriter, token string, expiry time.Time) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
const userKey contextKey = "web_user"
|
Name: cookieName,
|
||||||
const permissionsKey contextKey = "web_permissions"
|
Value: token,
|
||||||
|
Expires: expiry,
|
||||||
func setUser(r *http.Request, user *data.User) *http.Request {
|
Path: "/",
|
||||||
return r.WithContext(context.WithValue(r.Context(), userKey, user))
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUser(r *http.Request) *data.User {
|
func clearCookie(w http.ResponseWriter) {
|
||||||
user, ok := r.Context().Value(userKey).(*data.User)
|
http.SetCookie(w, &http.Cookie{
|
||||||
if !ok {
|
Name: cookieName,
|
||||||
return data.AnonymousUser
|
Value: "",
|
||||||
}
|
Expires: time.Unix(0, 0),
|
||||||
return user
|
Path: "/",
|
||||||
}
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
func setPermissions(r *http.Request, permissions data.Permissions) *http.Request {
|
MaxAge: -1,
|
||||||
return r.WithContext(context.WithValue(r.Context(), permissionsKey, permissions))
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func getPermissions(r *http.Request) data.Permissions {
|
|
||||||
permissions, ok := r.Context().Value(permissionsKey).(data.Permissions)
|
|
||||||
if !ok {
|
|
||||||
return data.Permissions{}
|
|
||||||
}
|
|
||||||
return permissions
|
|
||||||
}
|
}
|
||||||
|
|||||||
56
cmd/party/web_socket.go
Normal file
56
cmd/party/web_socket.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ws(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Upgrade error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
case t := <-ticker.C:
|
||||||
|
msg := map[string]interface{}{
|
||||||
|
"type": "server_tick",
|
||||||
|
"timestamp": t.Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.WriteJSON(msg); err != nil {
|
||||||
|
fmt.Println("Write error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
var msg Message
|
||||||
|
err := conn.ReadJSON(&msg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Read error:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Println(msg)
|
||||||
|
|
||||||
|
// Send a response
|
||||||
|
// response := fmt.Sprintf("Server time: %s", time.Now())
|
||||||
|
// if err := conn.WriteMessage(websocket.TextMessage, []byte(response)); err != nil {
|
||||||
|
// fmt.Println("Write error:", err)
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
11
web/html/error.page.tmpl
Normal file
11
web/html/error.page.tmpl
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "title"}}Error{{end}}
|
||||||
|
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">Error</h1>
|
||||||
|
<h2 class="page-title">{{ .We.Message }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{end}}
|
||||||
10
web/html/forbidden.page.tmpl
Normal file
10
web/html/forbidden.page.tmpl
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "title"}}Forbidden{{end}}
|
||||||
|
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">Forbidden</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{end}}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<div style="font-size:13px; color:var(--text-muted); margin-top:8px;">
|
<div style="font-size:13px; color:var(--text-muted); margin-top:8px;">
|
||||||
{{.Issue.StartTime.Format "02.01.2006"}} – {{.Issue.EndTime.Format "02.01.2006"}}
|
{{.Issue.StartTime.Format "02.01.2006"}} – {{.Issue.EndTime.Format "02.01.2006"}}
|
||||||
</div>
|
</div>
|
||||||
{{if .CanWriteIssues}}
|
{{if hasPermission "issues:write"}}
|
||||||
<div style="display:flex; gap:8px; margin-top:16px; flex-wrap:wrap;">
|
<div style="display:flex; gap:8px; margin-top:16px; flex-wrap:wrap;">
|
||||||
<button class="btn btn--danger"
|
<button class="btn btn--danger"
|
||||||
hx-delete="/issues/{{.Issue.ID}}"
|
hx-delete="/issues/{{.Issue.ID}}"
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
{{define "title"}}Abstimmungen{{end}}
|
{{define "title"}}Abstimmungen{{end}}
|
||||||
|
|
||||||
{{define "body"}}
|
{{define "body"}}
|
||||||
|
|
||||||
|
{{if hasPermission "issues:read"}}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1 class="page-title">Abstimmungen</h1>
|
<h1 class="page-title">Abstimmungen</h1>
|
||||||
<p class="page-subtitle">Aktuelle Abstimmungen der Digitalen Partei Österreich</p>
|
<p class="page-subtitle">Aktuelle Abstimmungen der Digitalen Partei Österreich</p>
|
||||||
@ -19,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:8px; margin-top:12px; flex-wrap:wrap;">
|
<div style="display:flex; gap:8px; margin-top:12px; flex-wrap:wrap;">
|
||||||
<a href="/issues/{{.ID}}" class="btn btn--primary">Details</a>
|
<a href="/issues/{{.ID}}" class="btn btn--primary">Details</a>
|
||||||
{{if $.CanWriteIssues}}
|
{{if hasPermission "issues:write"}}
|
||||||
<button class="btn btn--danger"
|
<button class="btn btn--danger"
|
||||||
hx-delete="/issues/{{.ID}}"
|
hx-delete="/issues/{{.ID}}"
|
||||||
hx-confirm="Abstimmung wirklich löschen?">Löschen</button>
|
hx-confirm="Abstimmung wirklich löschen?">Löschen</button>
|
||||||
@ -32,7 +34,11 @@
|
|||||||
<p style="color: var(--text-muted);">Keine Abstimmungen vorhanden.</p>
|
<p style="color: var(--text-muted);">Keine Abstimmungen vorhanden.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .CanWriteIssues}}
|
{{else}}
|
||||||
|
<p style="color: var(--text-muted);">Sie haben keine Berechtigung, die Abstimmungen einzusehen.</p>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if hasPermission "issues:write"}}
|
||||||
<details style="margin-top:32px;">
|
<details style="margin-top:32px;">
|
||||||
<summary class="btn btn--primary" style="display:inline-block; cursor:pointer; list-style:none;">+ Neue Abstimmung</summary>
|
<summary class="btn btn--primary" style="display:inline-block; cursor:pointer; list-style:none;">+ Neue Abstimmung</summary>
|
||||||
<div class="auth-card" style="max-width:520px; margin-top:16px;">
|
<div class="auth-card" style="max-width:520px; margin-top:16px;">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user