Compare commits
9
Commits
4e72beb433
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
074792be8f | ||
|
|
f77bcb3772 | ||
|
|
2900582eb4 | ||
|
|
65326b0ff5 | ||
|
|
4855255373 | ||
|
|
529bb6336e | ||
|
|
6f6eea5529 | ||
|
|
90cff1ac5b | ||
|
|
1913a0429f |
No files matched your search
@@ -27,14 +27,14 @@ func (api *Api) Authenticate(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
headerParts := strings.Split(authorizationHeader, " ")
|
headerParts := strings.Split(authorizationHeader, " ")
|
||||||
if len(headerParts) != 2 || headerParts[0] != "Bearer" {
|
if len(headerParts) != 2 || headerParts[0] != "Bearer" {
|
||||||
api.InvalidAuthenticationTokenResponse(w, r)
|
api.errorResponse(w, r, data.ErrInvalidAuthToken)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token := headerParts[1]
|
token := headerParts[1]
|
||||||
v := validator.New()
|
v := validator.New()
|
||||||
if data.ValidateTokenPlaintext(v, token); !v.Valid() {
|
if data.ValidateTokenPlaintext(v, token); !v.Valid() {
|
||||||
api.InvalidAuthenticationTokenResponse(w, r)
|
api.errorResponse(w, r, data.ErrInvalidAuthToken)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,9 +42,9 @@ func (api *Api) Authenticate(next http.Handler) http.Handler {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, data.ErrRecordNotFound):
|
case errors.Is(err, data.ErrRecordNotFound):
|
||||||
api.InvalidAuthenticationTokenResponse(w, r)
|
api.errorResponse(w, r, data.ErrInvalidAuthToken)
|
||||||
default:
|
default:
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -53,9 +53,9 @@ func (api *Api) Authenticate(next http.Handler) http.Handler {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, data.ErrRecordNotFound):
|
case errors.Is(err, data.ErrRecordNotFound):
|
||||||
api.InvalidCredentialsResponse(w, r)
|
api.errorResponse(w, r, data.ErrInvalidCredentials)
|
||||||
default:
|
default:
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"party.at/party/cmd/party/common"
|
"party.at/party/cmd/party/common"
|
||||||
|
"party.at/party/internal/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (api *Api) RegisterDeviceToken(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) RegisterDeviceToken(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -23,14 +23,14 @@ func (api *Api) RegisterDeviceToken(w http.ResponseWriter, r *http.Request) {
|
|||||||
"user_id": fmt.Sprint(user.ID),
|
"user_id": fmt.Sprint(user.ID),
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
api.BadRequestResponse(w, r, err)
|
api.errorResponse(w, r, data.ErrBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if input.Token == "" {
|
if input.Token == "" {
|
||||||
api.App.Logger.PrintInfo("register device token: empty token", map[string]string{
|
api.App.Logger.PrintInfo("register device token: empty token", map[string]string{
|
||||||
"user_id": fmt.Sprint(user.ID),
|
"user_id": fmt.Sprint(user.ID),
|
||||||
})
|
})
|
||||||
api.BadRequestResponse(w, r, errors.New("token must be provided"))
|
api.errorResponse(w, r, data.ErrNoToken)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ func (api *Api) RegisterDeviceToken(w http.ResponseWriter, r *http.Request) {
|
|||||||
api.App.Logger.PrintError(err, map[string]string{
|
api.App.Logger.PrintError(err, map[string]string{
|
||||||
"user_id": fmt.Sprint(user.ID),
|
"user_id": fmt.Sprint(user.ID),
|
||||||
})
|
})
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,17 +55,17 @@ func (api *Api) DeleteDeviceToken(w http.ResponseWriter, r *http.Request) {
|
|||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
if err := common.ReadJSON(w, r, &input); err != nil {
|
if err := common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.errorResponse(w, r, data.ErrBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if input.Token == "" {
|
if input.Token == "" {
|
||||||
api.BadRequestResponse(w, r, errors.New("token must be provided"))
|
api.errorResponse(w, r, data.ErrNoToken)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens, err := api.App.Models.DeviceTokens.GetForUser(user.ID)
|
tokens, err := api.App.Models.DeviceTokens.GetForUser(user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
owns := false
|
owns := false
|
||||||
@@ -76,12 +76,12 @@ func (api *Api) DeleteDeviceToken(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !owns {
|
if !owns {
|
||||||
api.NotFoundResponse(w, r)
|
api.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := api.App.Models.DeviceTokens.Delete(input.Token); err != nil {
|
if err := api.App.Models.DeviceTokens.Delete(input.Token); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-126
@@ -1,20 +1,13 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import(
|
import(
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"party.at/party/cmd/party/common"
|
"party.at/party/cmd/party/common"
|
||||||
|
"party.at/party/internal/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── 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) {
|
func (api *Api) LogError(r *http.Request, err error) {
|
||||||
api.App.Logger.PrintError(err, map[string]string{
|
api.App.Logger.PrintError(err, map[string]string{
|
||||||
"request_method": r.Method,
|
"request_method": r.Method,
|
||||||
@@ -22,124 +15,21 @@ func (api *Api) LogError(r *http.Request, err error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *Api) errorResponse(w http.ResponseWriter, r *http.Request, status int, ae apiError) {
|
func (api *Api) errorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
if err := common.WriteJSON(w, status, common.Envelope{"error": ae}, nil); err != nil {
|
api.App.LogError(r, err)
|
||||||
|
|
||||||
|
apiErr := &data.Error{
|
||||||
|
HttpCode: http.StatusInternalServerError,
|
||||||
|
Message: "the server encountered a problem",
|
||||||
|
}
|
||||||
|
|
||||||
|
var customErr *data.Error
|
||||||
|
if errors.As(err, &customErr) {
|
||||||
|
apiErr = customErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := common.WriteJSON(w, apiErr.HttpCode, common.Envelope{"error": apiErr}, nil); err != nil {
|
||||||
api.App.LogError(r, err)
|
api.App.LogError(r, err)
|
||||||
w.WriteHeader(500)
|
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",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,6 @@ func (api *Api) Healthcheck(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := common.WriteJSON(w, http.StatusOK, common.Envelope{"health_check": env}, nil); err != nil {
|
if err := common.WriteJSON(w, http.StatusOK, common.Envelope{"health_check": env}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+26
-50
@@ -27,18 +27,18 @@ func (api *Api) ListIssues(w http.ResponseWriter, r *http.Request) {
|
|||||||
input.Filters.SortSafelist = []string{"id", "-id", "title", "-title", "description", "-description"}
|
input.Filters.SortSafelist = []string{"id", "-id", "title", "-title", "description", "-description"}
|
||||||
|
|
||||||
if data.ValidateFilters(v, input.Filters); !v.Valid() {
|
if data.ValidateFilters(v, input.Filters); !v.Valid() {
|
||||||
api.FailedValidationResponse(w, r, v.Errors)
|
api.errorResponse(w, r, data.ErrValidationFailed.(*data.Error).WithDetails(v.Errors))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
issues, metadata, err := api.App.FetchIssues(input.Title, input.Filters, common.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.errorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = common.WriteJSON(w, http.StatusOK, common.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.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,54 +52,49 @@ func (api *Api) CreateIssue(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := common.ReadJSON(w, r, &input); err != nil {
|
if err := common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.errorResponse(w, r, data.ErrBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
issue, options, err := api.App.CreateIssue(input.Title, input.Description, input.StartTime, input.EndTime, input.Options)
|
issue, options, err := api.App.CreateIssue(input.Title, input.Description, input.StartTime, input.EndTime, input.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var ve *common.ValidationError
|
api.errorResponse(w, r, err)
|
||||||
if errors.As(err, &ve) {
|
|
||||||
api.FailedValidationResponse(w, r, ve.Errors)
|
|
||||||
} else {
|
|
||||||
api.ServerErrorResponse(w, r, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = common.WriteJSON(w, http.StatusCreated, common.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.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *Api) ReadIssue(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) ReadIssue(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := common.ReadIDParam(r)
|
id, err := common.ReadIDParam(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.NotFoundResponse(w, r)
|
api.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := api.App.GetIssue(id, common.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.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
} else {
|
} else {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = common.WriteJSON(w, http.StatusOK, common.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.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *Api) UpdateIssue(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) UpdateIssue(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := common.ReadIDParam(r)
|
id, err := common.ReadIDParam(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.NotFoundResponse(w, r)
|
api.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,78 +106,68 @@ func (api *Api) UpdateIssue(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = common.ReadJSON(w, r, &input); err != nil {
|
if err = common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.errorResponse(w, r, data.ErrBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
issue, err := api.App.UpdateIssue(id, input.Title, input.Description, input.StartTime, input.EndTime)
|
issue, err := api.App.UpdateIssue(id, input.Title, input.Description, input.StartTime, input.EndTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var ve *common.ValidationError
|
api.errorResponse(w, r, err)
|
||||||
switch {
|
|
||||||
case errors.As(err, &ve):
|
|
||||||
api.FailedValidationResponse(w, r, ve.Errors)
|
|
||||||
case errors.Is(err, data.ErrRecordNotFound):
|
|
||||||
api.NotFoundResponse(w, r)
|
|
||||||
case errors.Is(err, data.ErrEditConflict):
|
|
||||||
api.EditConflictResponse(w, r)
|
|
||||||
default:
|
|
||||||
api.ServerErrorResponse(w, r, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = common.WriteJSON(w, http.StatusOK, common.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.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *Api) DeleteIssue(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) DeleteIssue(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := common.ReadIDParam(r)
|
id, err := common.ReadIDParam(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.NotFoundResponse(w, r)
|
api.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.App.DeleteIssue(id); err != nil {
|
if err = api.App.DeleteIssue(id); err != nil {
|
||||||
if errors.Is(err, data.ErrRecordNotFound) {
|
if errors.Is(err, data.ErrRecordNotFound) {
|
||||||
api.NotFoundResponse(w, r)
|
api.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
} else {
|
} else {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = common.WriteJSON(w, http.StatusOK, common.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.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *Api) ReadIssuePubKey(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) ReadIssuePubKey(w http.ResponseWriter, r *http.Request) {
|
||||||
id, err := common.ReadIDParam(r)
|
id, err := common.ReadIDParam(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.NotFoundResponse(w, r)
|
api.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pubKey, err := api.App.GetIssuePublicKey(id)
|
pubKey, err := api.App.GetIssuePublicKey(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, data.ErrRecordNotFound) {
|
if errors.Is(err, data.ErrRecordNotFound) {
|
||||||
api.NotFoundResponse(w, r)
|
api.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
} else {
|
} else {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = common.WriteJSON(w, http.StatusOK, common.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.errorResponse(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 := common.ReadIDParam(r)
|
id, err := common.ReadIDParam(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.NotFoundResponse(w, r)
|
api.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,26 +175,17 @@ func (api *Api) BlindSignIssueVote(w http.ResponseWriter, r *http.Request) {
|
|||||||
BlindedVote []byte `json:"blinded_vote"`
|
BlindedVote []byte `json:"blinded_vote"`
|
||||||
}
|
}
|
||||||
if err = common.ReadJSON(w, r, &input); err != nil {
|
if err = common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.errorResponse(w, r, data.ErrBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
signed, err := api.App.BlindSign(id, input.BlindedVote, common.GetUser(r))
|
signed, err := api.App.BlindSign(id, input.BlindedVote, common.GetUser(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
api.errorResponse(w, r, err)
|
||||||
case errors.Is(err, data.ErrRecordNotFound):
|
|
||||||
api.NotFoundResponse(w, r)
|
|
||||||
case errors.Is(err, data.ErrDuplicateBlindSign):
|
|
||||||
api.AlreadyBlindSignedResponse(w, r)
|
|
||||||
case errors.Is(err, data.ErrInvalidBlindedVote):
|
|
||||||
api.BlindedVoteOutOfRangeResponse(w, r)
|
|
||||||
default:
|
|
||||||
api.ServerErrorResponse(w, r, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = common.WriteJSON(w, http.StatusOK, common.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.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+14
-12
@@ -1,13 +1,15 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"party.at/party/cmd/party/common"
|
|
||||||
"time"
|
|
||||||
"sync"
|
|
||||||
"golang.org/x/time/rate"
|
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
"party.at/party/cmd/party/common"
|
||||||
|
"party.at/party/internal/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (api *Api) RecoverPanic(next http.Handler) http.Handler {
|
func (api *Api) RecoverPanic(next http.Handler) http.Handler {
|
||||||
@@ -15,7 +17,7 @@ func (api *Api) RecoverPanic(next http.Handler) http.Handler {
|
|||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
w.Header().Set("Connection", "close")
|
w.Header().Set("Connection", "close")
|
||||||
api.ServerErrorResponse(w, r, fmt.Errorf("%s", err))
|
api.errorResponse(w, r, fmt.Errorf("%s", err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
@@ -25,7 +27,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 common.GetUser(r).IsAnonymous() {
|
if common.GetUser(r).IsAnonymous() {
|
||||||
api.AuthenticationRequiredResponse(w, r)
|
api.errorResponse(w, r, data.ErrAuthRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
@@ -35,7 +37,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 !common.GetUser(r).Activated {
|
if !common.GetUser(r).Activated {
|
||||||
api.InactiveAccountResponse(w, r)
|
api.errorResponse(w, r, data.ErrInactiveAccount)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
@@ -47,11 +49,11 @@ func (api *Api) RequirePermission(code string, next http.HandlerFunc) http.Handl
|
|||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
permissions, err := api.App.Models.Permissions.GetAllForUser(common.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.errorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !permissions.Include(code) {
|
if !permissions.Include(code) {
|
||||||
api.NotPermittedResponse(w, r)
|
api.errorResponse(w, r, data.ErrNotPermitted)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
@@ -85,7 +87,7 @@ func (api *Api) RateLimit(next http.Handler) http.Handler {
|
|||||||
if api.App.LimiterEnabled {
|
if api.App.LimiterEnabled {
|
||||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +98,7 @@ func (api *Api) RateLimit(next http.Handler) http.Handler {
|
|||||||
clients[ip].lastSeen = time.Now()
|
clients[ip].lastSeen = time.Now()
|
||||||
if !clients[ip].limiter.Allow() {
|
if !clients[ip].limiter.Allow() {
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
api.RateLimitExceededResponse(w, r)
|
api.errorResponse(w, r, data.ErrRateLimitExceeded)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
func (api *Api) ListMPs(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) ListMPs(w http.ResponseWriter, r *http.Request) {
|
||||||
members, err := api.App.Parlament.ListNRMembers("XXVIII")
|
members, err := api.App.Parlament.ListNRMembers("XXVIII")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +25,6 @@ func (api *Api) ListMPs(w http.ResponseWriter, r *http.Request) {
|
|||||||
"members": members,
|
"members": members,
|
||||||
"total": len(members),
|
"total": len(members),
|
||||||
}, nil); err != nil {
|
}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"party.at/party/cmd/party/common"
|
"party.at/party/cmd/party/common"
|
||||||
|
"party.at/party/internal/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (api *Api) GetParlVoteDetail(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) GetParlVoteDetail(w http.ResponseWriter, r *http.Request) {
|
||||||
routePath := r.PathValue("path")
|
routePath := r.PathValue("path")
|
||||||
routePath = strings.TrimPrefix(routePath, "/")
|
routePath = strings.TrimPrefix(routePath, "/")
|
||||||
if routePath == "" {
|
if routePath == "" {
|
||||||
api.BadRequestResponse(w, r, errors.New("path is required"))
|
api.errorResponse(w, r, data.ErrBadRequest)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,15 +21,15 @@ func (api *Api) GetParlVoteDetail(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
detail, err := api.App.Parlament.GetDocumentVote(path)
|
detail, err := api.App.Parlament.GetDocumentVote(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if detail == nil {
|
if detail == nil {
|
||||||
api.NotFoundResponse(w, r)
|
api.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := common.WriteJSON(w, http.StatusOK, common.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.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ func (api *Api) ListParlVotes(w http.ResponseWriter, r *http.Request) {
|
|||||||
Period: []string{parlament.CurrentPeriod},
|
Period: []string{parlament.CurrentPeriod},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ func (api *Api) ListParlVotes(w http.ResponseWriter, r *http.Request) {
|
|||||||
"total_with_votes": totalWithVotes,
|
"total_with_votes": totalWithVotes,
|
||||||
"party_stats": partyStats,
|
"party_stats": partyStats,
|
||||||
}, nil); err != nil {
|
}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func (api *Api) CreateAuthenticationToken(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := common.ReadJSON(w, r, &input); err != nil {
|
if err := common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.errorResponse(w, r, data.ErrBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ func (api *Api) CreateAuthenticationToken(w http.ResponseWriter, r *http.Request
|
|||||||
v.Check(input.Email != "", "email", "must be provided")
|
v.Check(input.Email != "", "email", "must be provided")
|
||||||
v.Check(input.Password != "", "password", "must be provided")
|
v.Check(input.Password != "", "password", "must be provided")
|
||||||
if !v.Valid() {
|
if !v.Valid() {
|
||||||
api.FailedValidationResponse(w, r, v.Errors)
|
api.errorResponse(w, r, data.ErrValidationFailed.(*data.Error).WithDetails(v.Errors))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,15 +33,15 @@ func (api *Api) CreateAuthenticationToken(w http.ResponseWriter, r *http.Request
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, data.ErrInvalidCredentials):
|
case errors.Is(err, data.ErrInvalidCredentials):
|
||||||
api.InvalidCredentialsResponse(w, r)
|
api.errorResponse(w, r, data.ErrInvalidCredentials)
|
||||||
default:
|
default:
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = common.WriteJSON(w, http.StatusCreated, common.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.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,23 +49,23 @@ func (api *Api) DeleteAuthenticationToken(w http.ResponseWriter, r *http.Request
|
|||||||
authHeader := r.Header.Get("Authorization")
|
authHeader := r.Header.Get("Authorization")
|
||||||
parts := strings.Split(authHeader, " ")
|
parts := strings.Split(authHeader, " ")
|
||||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
api.InvalidAuthenticationTokenResponse(w, r)
|
api.errorResponse(w, r, data.ErrInvalidAuthToken)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token := parts[1]
|
token := parts[1]
|
||||||
v := validator.New()
|
v := validator.New()
|
||||||
if data.ValidateTokenPlaintext(v, token); !v.Valid() {
|
if data.ValidateTokenPlaintext(v, token); !v.Valid() {
|
||||||
api.InvalidAuthenticationTokenResponse(w, r)
|
api.errorResponse(w, r, data.ErrInvalidAuthToken)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := api.App.DeleteToken(token); err != nil {
|
if err := api.App.DeleteToken(token); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := common.WriteJSON(w, http.StatusOK, common.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.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+31
-85
@@ -4,7 +4,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
|
|
||||||
"party.at/party/cmd/party/common"
|
"party.at/party/cmd/party/common"
|
||||||
"party.at/party/internal/data"
|
"party.at/party/internal/data"
|
||||||
@@ -25,148 +24,103 @@ func (api *Api) ListUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
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() {
|
||||||
api.FailedValidationResponse(w, r, v.Errors)
|
api.errorResponse(w, r, data.ErrValidationFailed.(*data.Error).WithDetails(v.Errors))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
users, metadata, err := api.App.ListUsers(input.Filters)
|
users, metadata, err := api.App.ListUsers(input.Filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = common.WriteJSON(w, http.StatusOK, common.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.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *Api) CreateUser(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
var input struct {
|
var input common.RegisterUserInput
|
||||||
ProviderId int64 `json:"provider_id"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
PhoneNumber string `json:"phone_number"`
|
|
||||||
Country string `json:"country"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
AltName *string `json:"alt_name"`
|
|
||||||
DateOfBirth time.Time `json:"date_of_birth"`
|
|
||||||
Address string `json:"address"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := common.ReadJSON(w, r, &input); err != nil {
|
if err := common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.errorResponse(w, r, data.ErrBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, authToken, err := api.App.RegisterUser(common.RegisterUserInput{
|
user, authToken, err := api.App.RegisterUser(input)
|
||||||
ProviderID: input.ProviderId,
|
|
||||||
Username: input.Username,
|
|
||||||
PhoneNumber: input.PhoneNumber,
|
|
||||||
Country: input.Country,
|
|
||||||
Email: input.Email,
|
|
||||||
Password: input.Password,
|
|
||||||
Name: input.Name,
|
|
||||||
AltName: input.AltName,
|
|
||||||
DateOfBirth: input.DateOfBirth,
|
|
||||||
Address: input.Address,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var ve *common.ValidationError
|
api.errorResponse(w, r, err)
|
||||||
switch {
|
|
||||||
case errors.As(err, &ve):
|
|
||||||
api.FailedValidationResponse(w, r, ve.Errors)
|
|
||||||
case errors.Is(err, data.ErrDuplicateEmail):
|
|
||||||
v := validator.New()
|
|
||||||
v.AddError("email", "a user with this email address already exists")
|
|
||||||
api.FailedValidationResponse(w, r, v.Errors)
|
|
||||||
case errors.Is(err, data.ErrDuplicateUser):
|
|
||||||
v := validator.New()
|
|
||||||
v.AddError("username", "a user with this username already exists")
|
|
||||||
api.FailedValidationResponse(w, r, v.Errors)
|
|
||||||
default:
|
|
||||||
api.ServerErrorResponse(w, r, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
api.App.Logger.PrintInfo("user registered", map[string]string{
|
||||||
|
"user_id": strconv.FormatInt(user.ID, 10),
|
||||||
|
"email": user.Email,
|
||||||
|
})
|
||||||
|
|
||||||
if err = common.WriteJSON(w, http.StatusCreated, common.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.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *Api) ReadUser(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) ReadUser(w http.ResponseWriter, r *http.Request) {
|
||||||
var id int64
|
user := common.GetUser(r)
|
||||||
|
|
||||||
param := r.PathValue("id")
|
param := r.PathValue("id")
|
||||||
|
|
||||||
|
var id int64
|
||||||
if param == "me" {
|
if param == "me" {
|
||||||
id = common.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)
|
||||||
if err != nil || id < 1 {
|
if err != nil || id < 1 {
|
||||||
api.NotFoundResponse(w, r)
|
api.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if common.GetUser(r).ID != id {
|
target, err := api.App.GetUser(user, id)
|
||||||
api.errorResponse(w, r, http.StatusForbidden, apiError{
|
|
||||||
Code: common.ErrCodeNotPermitted,
|
|
||||||
Message: "your user account doesn't have the necessary permissions to access this resource",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := api.App.GetUser(id)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, data.ErrRecordNotFound) {
|
if errors.Is(err, data.ErrRecordNotFound) {
|
||||||
api.NotFoundResponse(w, r)
|
api.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
} else {
|
} else {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = common.WriteJSON(w, http.StatusOK, common.Envelope{"user": user}, nil); err != nil {
|
if err = common.WriteJSON(w, http.StatusOK, common.Envelope{"user": target}, nil); err != nil {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (api *Api) ReadAuthenticatedUser(w http.ResponseWriter, r *http.Request) {
|
// func (api *Api) ReadAuthenticatedUser(w http.ResponseWriter, r *http.Request) {
|
||||||
// user := common.GetUser(r).ID
|
// user := common.GetUser(r).ID
|
||||||
// // if err != nil {
|
|
||||||
// // if errors.Is(err, data.ErrRecordNotFound) {
|
|
||||||
// // api.NotFoundResponse(w, r)
|
|
||||||
// // } else {
|
|
||||||
// // api.ServerErrorResponse(w, r, err)
|
|
||||||
// // }
|
|
||||||
// // return
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// if err := common.WriteJSON(w, http.StatusOK, common.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.errorResponse(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 := common.ReadIDParam(r)
|
id, err := common.ReadIDParam(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.NotFoundResponse(w, r)
|
api.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.App.DeleteUser(id); err != nil {
|
if err = api.App.DeleteUser(id); err != nil {
|
||||||
if errors.Is(err, data.ErrRecordNotFound) {
|
if errors.Is(err, data.ErrRecordNotFound) {
|
||||||
api.NotFoundResponse(w, r)
|
api.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
} else {
|
} else {
|
||||||
api.ServerErrorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = common.WriteJSON(w, http.StatusOK, common.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.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,31 +130,23 @@ func (api *Api) ActivateUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := common.ReadJSON(w, r, &input); err != nil {
|
if err := common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.errorResponse(w, r, data.ErrBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
v := validator.New()
|
v := validator.New()
|
||||||
if data.ValidateTokenPlaintext(v, input.TokenPlaintext); !v.Valid() {
|
if data.ValidateTokenPlaintext(v, input.TokenPlaintext); !v.Valid() {
|
||||||
api.FailedValidationResponse(w, r, v.Errors)
|
api.errorResponse(w, r, data.ErrValidationFailed.(*data.Error).WithDetails(v.Errors))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := api.App.ActivateUser(input.TokenPlaintext)
|
user, err := api.App.ActivateUser(input.TokenPlaintext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
api.errorResponse(w, r, err)
|
||||||
case errors.Is(err, data.ErrRecordNotFound):
|
|
||||||
v.AddError("token", "invalid or expired activation token")
|
|
||||||
api.FailedValidationResponse(w, r, v.Errors)
|
|
||||||
case errors.Is(err, data.ErrEditConflict):
|
|
||||||
api.EditConflictResponse(w, r)
|
|
||||||
default:
|
|
||||||
api.ServerErrorResponse(w, r, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = common.WriteJSON(w, http.StatusOK, common.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.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+9
-13
@@ -1,7 +1,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"party.at/party/internal/data"
|
"party.at/party/internal/data"
|
||||||
@@ -9,6 +8,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (api *Api) Vote(w http.ResponseWriter, r *http.Request) {
|
func (api *Api) Vote(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if authHeader := r.Header.Get("Authorization"); authHeader != "" {
|
||||||
|
api.errorResponse(w, r, data.ErrAuthenticatedVoteRejected)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var input struct {
|
var input struct {
|
||||||
IssueID int64 `json:"issue_id"`
|
IssueID int64 `json:"issue_id"`
|
||||||
OptionID int64 `json:"option_id"`
|
OptionID int64 `json:"option_id"`
|
||||||
@@ -17,25 +21,17 @@ func (api *Api) Vote(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := common.ReadJSON(w, r, &input); err != nil {
|
if err := common.ReadJSON(w, r, &input); err != nil {
|
||||||
api.BadRequestResponse(w, r, err)
|
api.errorResponse(w, r, data.ErrBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := api.App.CastVote(input.IssueID, input.OptionID, input.Nonce, input.Signature); err != nil {
|
if err := api.App.CastVote(input.IssueID, input.OptionID, input.Nonce, input.Signature); err != nil {
|
||||||
switch {
|
api.errorResponse(w, r, err)
|
||||||
case errors.Is(err, data.ErrRecordNotFound):
|
|
||||||
api.NotFoundResponse(w, r)
|
|
||||||
case errors.Is(err, data.ErrInvalidSignature):
|
|
||||||
api.InvalidSignatureResponse(w, r)
|
|
||||||
case errors.Is(err, data.ErrDuplicateVote):
|
|
||||||
api.AlreadyVotedResponse(w, r)
|
|
||||||
default:
|
|
||||||
api.ServerErrorResponse(w, r, err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := common.WriteJSON(w, http.StatusCreated, common.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.errorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,14 +64,6 @@ type Application struct {
|
|||||||
CORSTrustedOrigins []string
|
CORSTrustedOrigins []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ValidationError struct {
|
|
||||||
Errors map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ValidationError) Error() string {
|
|
||||||
return "validation failed"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *Application) background(fn func()) {
|
func (app *Application) background(fn func()) {
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -87,6 +79,7 @@ type contextKey string
|
|||||||
|
|
||||||
const userKey contextKey = "web_user"
|
const userKey contextKey = "web_user"
|
||||||
const permissionsKey contextKey = "web_permissions"
|
const permissionsKey contextKey = "web_permissions"
|
||||||
|
const RequestBodyKey contextKey = "request_body"
|
||||||
|
|
||||||
func SetUser(r *http.Request, user *data.User) *http.Request {
|
func SetUser(r *http.Request, user *data.User) *http.Request {
|
||||||
return r.WithContext(context.WithValue(r.Context(), userKey, user))
|
return r.WithContext(context.WithValue(r.Context(), userKey, user))
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
type ErrorCode int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// 401 variants
|
|
||||||
ErrCodeInvalidCredentials ErrorCode = 4011
|
|
||||||
ErrCodeInvalidAuthToken ErrorCode = 4012
|
|
||||||
ErrCodeAuthRequired ErrorCode = 4013
|
|
||||||
|
|
||||||
// 403 variants
|
|
||||||
ErrCodeInactiveAccount ErrorCode = 4031
|
|
||||||
ErrCodeNotPermitted ErrorCode = 4032
|
|
||||||
|
|
||||||
// 409 variants
|
|
||||||
ErrCodeEditConflict ErrorCode = 4091
|
|
||||||
ErrCodeAlreadyVoted ErrorCode = 4092
|
|
||||||
ErrCodeAlreadyBlindSigned ErrorCode = 4093
|
|
||||||
ErrCodeVoteAlreadyCast ErrorCode = 4094
|
|
||||||
|
|
||||||
// 422 variants
|
|
||||||
ErrCodeValidationFailed ErrorCode = 4221
|
|
||||||
ErrCodeBlindedVoteRange ErrorCode = 4222
|
|
||||||
ErrCodeInvalidSignature ErrorCode = 4223
|
|
||||||
)
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"party.at/party/internal/validator"
|
"party.at/party/internal/validator"
|
||||||
|
"party.at/party/internal/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Envelope map[string]interface{}
|
type Envelope map[string]interface{}
|
||||||
@@ -49,14 +51,14 @@ func ReadJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
|
|||||||
case errors.As(err, &syntaxError):
|
case errors.As(err, &syntaxError):
|
||||||
return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset)
|
return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset)
|
||||||
case errors.Is(err, io.ErrUnexpectedEOF):
|
case errors.Is(err, io.ErrUnexpectedEOF):
|
||||||
return errors.New("body contains badly-formed JSON")
|
return data.ErrBadlyFormedJSON
|
||||||
case errors.As(err, &unmarshalTypeError):
|
case errors.As(err, &unmarshalTypeError):
|
||||||
if unmarshalTypeError.Field != "" {
|
if unmarshalTypeError.Field != "" {
|
||||||
return fmt.Errorf("body contains incorrect JSON type for field %q", 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)
|
return fmt.Errorf("body contains incorrect JSON type (at character %d)", unmarshalTypeError.Offset)
|
||||||
case errors.Is(err, io.EOF):
|
case errors.Is(err, io.EOF):
|
||||||
return errors.New("body must not be empty")
|
return data.ErrBodyEmpty
|
||||||
case strings.HasPrefix(err.Error(), "json: unknown field "):
|
case strings.HasPrefix(err.Error(), "json: unknown field "):
|
||||||
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
|
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
|
||||||
return fmt.Errorf("body contains unknown key %s", fieldName)
|
return fmt.Errorf("body contains unknown key %s", fieldName)
|
||||||
@@ -70,7 +72,7 @@ func ReadJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
|
|||||||
}
|
}
|
||||||
err = dec.Decode(&struct{}{})
|
err = dec.Decode(&struct{}{})
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
return errors.New("body must only contain a single JSON value")
|
return data.ErrSingleValue
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -91,7 +93,7 @@ func GenerateIssueKey() ([]byte, int, []byte, error) {
|
|||||||
func ReadIDParam(r *http.Request) (int64, error) {
|
func ReadIDParam(r *http.Request) (int64, error) {
|
||||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil || id < 1 {
|
if err != nil || id < 1 {
|
||||||
return 0, errors.New("invalid id parameter")
|
return 0, data.ErrInvalidID
|
||||||
}
|
}
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
@@ -127,8 +129,18 @@ func ReadCSV(qs url.Values, key string, defaultValue []string) []string {
|
|||||||
// ── Error responses ──────────────────────────────────────────────────────────
|
// ── Error responses ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func (app *Application) LogError(r *http.Request, err error) {
|
func (app *Application) LogError(r *http.Request, err error) {
|
||||||
app.Logger.PrintError(err, map[string]string{
|
props := map[string]string{
|
||||||
"request_method": r.Method,
|
"request_method": r.Method,
|
||||||
"request_url": r.URL.String(),
|
"request_url": r.URL.String(),
|
||||||
})
|
}
|
||||||
|
if buf, ok := r.Context().Value(RequestBodyKey).(*bytes.Buffer); ok && buf.Len() > 0 {
|
||||||
|
props["request_body"] = buf.String()
|
||||||
|
}
|
||||||
|
var apiErr *data.Error
|
||||||
|
if errors.As(err, &apiErr) {
|
||||||
|
for k, v := range apiErr.Details {
|
||||||
|
props[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.Logger.PrintError(err, props)
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ func (app *Application) CreateIssue(title, description string, startTime, endTim
|
|||||||
data.ValidateOption(v, option)
|
data.ValidateOption(v, option)
|
||||||
}
|
}
|
||||||
if data.ValidateIssue(v, issue); !v.Valid() {
|
if data.ValidateIssue(v, issue); !v.Valid() {
|
||||||
return nil, nil, &ValidationError{Errors: v.Errors}
|
return nil, nil, data.ErrValidationFailed.(*data.Error).WithDetails(v.Errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
n, e, privatePEM, err := generateIssueKey()
|
n, e, privatePEM, err := generateIssueKey()
|
||||||
@@ -145,7 +145,7 @@ func (app *Application) UpdateIssue(id int64, title, description *string, startT
|
|||||||
|
|
||||||
v := validator.New()
|
v := validator.New()
|
||||||
if data.ValidateIssue(v, issue); !v.Valid() {
|
if data.ValidateIssue(v, issue); !v.Valid() {
|
||||||
return nil, &ValidationError{Errors: v.Errors}
|
return nil, data.ErrValidationFailed.(*data.Error).WithDetails(v.Errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = app.Models.Issues.Update(issue); err != nil {
|
if err = app.Models.Issues.Update(issue); err != nil {
|
||||||
@@ -173,6 +173,10 @@ func (app *Application) BlindSign(issueID int64, blindedVote []byte, user *data.
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if issue.StartTime.After(time.Now()) {
|
||||||
|
return nil, data.ErrHasNotStarted
|
||||||
|
}
|
||||||
|
|
||||||
blindSign := &data.BlindSign{UserID: user.ID, IssueID: issue.ID}
|
blindSign := &data.BlindSign{UserID: user.ID, IssueID: issue.ID}
|
||||||
if err = app.Models.BlindSigns.Insert(blindSign); err != nil {
|
if err = app.Models.BlindSigns.Insert(blindSign); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"expvar"
|
"expvar"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -9,6 +12,15 @@ import (
|
|||||||
"github.com/felixge/httpsnoop"
|
"github.com/felixge/httpsnoop"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (app *Application) CaptureBody(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
r.Body = io.NopCloser(io.TeeReader(r.Body, buf))
|
||||||
|
ctx := context.WithValue(r.Context(), RequestBodyKey, buf)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
metricsOnce sync.Once
|
metricsOnce sync.Once
|
||||||
metricTotalRequests *expvar.Int
|
metricTotalRequests *expvar.Int
|
||||||
|
|||||||
+62
-15
@@ -8,16 +8,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type RegisterUserInput struct {
|
type RegisterUserInput struct {
|
||||||
ProviderID int64
|
ProviderID int64 `json:"provider_id"`
|
||||||
Username string
|
Username string `json:"username"`
|
||||||
PhoneNumber string
|
PhoneNumber string `json:"phone_number"`
|
||||||
Country string
|
Country string `json:"country"`
|
||||||
Email string
|
Email string `json:"email"`
|
||||||
Password string
|
Password string `json:"password"`
|
||||||
Name string
|
Name string `json:"name"`
|
||||||
AltName *string
|
AltName *string `json:"alt_name"`
|
||||||
DateOfBirth time.Time
|
DateOfBirth time.Time `json:"date_of_birth"`
|
||||||
Address string
|
Address string `json:"address"`
|
||||||
|
Role string `json:"role"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *Application) RegisterUser(input RegisterUserInput) (*data.User, *data.Token, error) {
|
func (app *Application) RegisterUser(input RegisterUserInput) (*data.User, *data.Token, error) {
|
||||||
@@ -45,7 +46,7 @@ func (app *Application) RegisterUser(input RegisterUserInput) (*data.User, *data
|
|||||||
data.ValidateUser(v, user)
|
data.ValidateUser(v, user)
|
||||||
data.ValidateUserIdentity(v, userIdentity)
|
data.ValidateUserIdentity(v, userIdentity)
|
||||||
if !v.Valid() {
|
if !v.Valid() {
|
||||||
return nil, nil, &ValidationError{Errors: v.Errors}
|
return nil, nil, data.ErrValidationFailed.(*data.Error).WithDetails(v.Errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := app.Models.Users.ExecuteRegistrationTx(user, userIdentity); err != nil {
|
if err := app.Models.Users.ExecuteRegistrationTx(user, userIdentity); err != nil {
|
||||||
@@ -53,8 +54,8 @@ func (app *Application) RegisterUser(input RegisterUserInput) (*data.User, *data
|
|||||||
}
|
}
|
||||||
|
|
||||||
role := "viewer"
|
role := "viewer"
|
||||||
if app.Config.Env == "development" {
|
if app.Config.Env == "development" && input.Role != "" {
|
||||||
role = ""
|
role = input.Role
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := app.Models.Roles.AssignToUser(user.ID, role); err != nil {
|
if err := app.Models.Roles.AssignToUser(user.ID, role); err != nil {
|
||||||
@@ -85,8 +86,54 @@ func (app *Application) RegisterUser(input RegisterUserInput) (*data.User, *data
|
|||||||
return user, authToken, nil
|
return user, authToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *Application) GetUser(id int64) (*data.User, error) {
|
func (app *Application) GetUser(user *data.User, id int64) (*data.User, error) {
|
||||||
return app.Models.Users.Get(id)
|
var ret data.User
|
||||||
|
|
||||||
|
roles, err := app.Models.Roles.GetAllForUser(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return &ret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
target, err := app.Models.Users.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return &ret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.ID == target.ID {
|
||||||
|
return target, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if roles[0].Code == "viewer" {
|
||||||
|
return nil, data.ErrNotPermitted
|
||||||
|
}
|
||||||
|
|
||||||
|
if roles[0].Code == "contributor" {
|
||||||
|
return nil, data.ErrInvalidCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
if roles[0].Code == "member_of_parliament" {
|
||||||
|
ret.Email = target.Email
|
||||||
|
ret.PhoneNumber = target.PhoneNumber
|
||||||
|
ret.Country = target.Country
|
||||||
|
ret.Name = target.Name
|
||||||
|
ret.AltName = target.AltName
|
||||||
|
ret.DateOfBirth = target.DateOfBirth
|
||||||
|
ret.Address = target.Address
|
||||||
|
}
|
||||||
|
|
||||||
|
if roles[0].Code == "party_leadership" {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if roles[0].Code == "admin" {
|
||||||
|
ret.ID = target.ID
|
||||||
|
ret.LastLogin = target.LastLogin
|
||||||
|
ret.Activated = target.Activated
|
||||||
|
ret.Created = target.Created
|
||||||
|
ret.Version = target.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *Application) ListUsers(filters data.Filters) ([]*data.User, data.Metadata, error) {
|
func (app *Application) ListUsers(filters data.Filters) ([]*data.User, data.Metadata, error) {
|
||||||
|
|||||||
+2
-2
@@ -40,7 +40,7 @@ func routes(app *common.Application) http.Handler {
|
|||||||
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())
|
||||||
|
|
||||||
apiChain := app.Metrics(api.RecoverPanic(app.EnableCORS(api.RateLimit(api.Authenticate(apiMux)))))
|
apiChain := app.Metrics(api.RecoverPanic(app.CaptureBody(app.EnableCORS(api.RateLimit(api.Authenticate(apiMux))))))
|
||||||
|
|
||||||
// ── Web router ──────────────────────────────────────────────────────────
|
// ── Web router ──────────────────────────────────────────────────────────
|
||||||
web := web.Web{
|
web := web.Web{
|
||||||
@@ -63,7 +63,7 @@ func routes(app *common.Application) http.Handler {
|
|||||||
webMux.HandleFunc("GET /users", web.RequirePermission("users:read", web.UsersPage))
|
webMux.HandleFunc("GET /users", web.RequirePermission("users:read", web.UsersPage))
|
||||||
webMux.HandleFunc("GET /users/me", web.RequireAuthenticatedUser(web.ProfilePage))
|
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("PUT /users/activated", web.ActivateUserAction)
|
||||||
webMux.HandleFunc("DELETE /users/{id}", web.RequirePermission("users:write", 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)
|
||||||
|
|||||||
@@ -92,8 +92,14 @@ func (ts *testServer) get(t *testing.T, path string) (int, http.Header, []byte)
|
|||||||
return rs.StatusCode, rs.Header, body
|
return rs.StatusCode, rs.Header, body
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *testServer) registerAndLogin(t *testing.T, email, password string) string {
|
func (ts *testServer) registerAndLogin(t *testing.T, email, password string, roles ...string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
var role string
|
||||||
|
if len(role) > 0 {
|
||||||
|
role = roles[0]
|
||||||
|
}
|
||||||
|
|
||||||
registerBody := map[string]any{
|
registerBody := map[string]any{
|
||||||
"email": email,
|
"email": email,
|
||||||
"password": password,
|
"password": password,
|
||||||
@@ -101,6 +107,7 @@ func (ts *testServer) registerAndLogin(t *testing.T, email, password string) str
|
|||||||
"name": "Test User",
|
"name": "Test User",
|
||||||
"alt_name": "",
|
"alt_name": "",
|
||||||
"provider_id": 1,
|
"provider_id": 1,
|
||||||
|
"role": role,
|
||||||
}
|
}
|
||||||
code, _, body := ts.postJSON(t, "/v1/users", registerBody)
|
code, _, body := ts.postJSON(t, "/v1/users", registerBody)
|
||||||
if code != http.StatusCreated {
|
if code != http.StatusCreated {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
// "net/http"
|
||||||
|
// "strconv"
|
||||||
|
"testing"
|
||||||
|
// "time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadUserHandler(t *testing.T) {
|
||||||
|
app := newTestApplication(t)
|
||||||
|
ts := newTestServer(t, app, routes(app))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
token := ts.registerAndLogin(t, uniqueEmail(), "pa$$word123", "viewer")
|
||||||
|
|
||||||
|
code, _, body := ts.getWithToken(t, "/v1/users/1", token)
|
||||||
|
|
||||||
|
if code != 200 {
|
||||||
|
t.Errorf("want %d; got %d", 403, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Contains(body, []byte("An old silent pond...")) {
|
||||||
|
t.Errorf("want body to contain %q; got %q", "", string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-120
@@ -1,8 +1,8 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import(
|
import(
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"party.at/party/internal/data"
|
"party.at/party/internal/data"
|
||||||
"party.at/party/cmd/party/common"
|
"party.at/party/cmd/party/common"
|
||||||
@@ -10,12 +10,6 @@ import(
|
|||||||
|
|
||||||
// ── Error responses ──────────────────────────────────────────────────────────
|
// ── 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) {
|
func (web *Web) LogError(r *http.Request, err error) {
|
||||||
web.App.Logger.PrintError(err, map[string]string{
|
web.App.Logger.PrintError(err, map[string]string{
|
||||||
"request_method": r.Method,
|
"request_method": r.Method,
|
||||||
@@ -23,131 +17,32 @@ func (web *Web) LogError(r *http.Request, err error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) errorResponse(w http.ResponseWriter, r *http.Request, status int, we webError) {
|
func (web *Web) errorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
// if err := common.WriteJSON(w, status, common.Envelope{"error": we}, nil); err != nil {
|
displayError := data.Error{
|
||||||
// web.LogError(r, err)
|
HttpCode: http.StatusInternalServerError,
|
||||||
// w.WriteHeader(500)
|
Message: "the server encountered a problem and could not process your request",
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
var customErr *data.Error
|
||||||
|
if errors.As(err, &customErr) {
|
||||||
|
displayError = *customErr
|
||||||
|
}
|
||||||
|
|
||||||
user := common.GetUser(r)
|
user := common.GetUser(r)
|
||||||
|
|
||||||
web.render(w, r, http.StatusOK, "error", struct {
|
web.render(w, r, displayError.HttpCode, "error", struct {
|
||||||
AuthenticatedUser *data.User
|
AuthenticatedUser *data.User
|
||||||
FormErrors []string
|
|
||||||
IsDevelopment bool
|
IsDevelopment bool
|
||||||
Status int
|
Error data.Error
|
||||||
We webError
|
|
||||||
}{
|
}{
|
||||||
AuthenticatedUser: user,
|
AuthenticatedUser: user,
|
||||||
IsDevelopment: web.App.Config.Env == "development",
|
IsDevelopment: web.App.Config.Env == "development",
|
||||||
Status: status,
|
Error: displayError,
|
||||||
We: we,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) ServerErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
func (web *Web) ServerErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
web.LogError(r, err)
|
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, err)
|
||||||
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",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
@@ -93,6 +93,7 @@ func (web *Web) IssuePage(w http.ResponseWriter, r *http.Request) {
|
|||||||
id, err := common.ReadIDParam(r)
|
id, err := common.ReadIDParam(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
web.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,9 +240,11 @@ func (web *Web) BlindSignIssueVote(w http.ResponseWriter, r *http.Request) {
|
|||||||
case errors.Is(err, data.ErrRecordNotFound):
|
case errors.Is(err, data.ErrRecordNotFound):
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
case errors.Is(err, data.ErrDuplicateBlindSign):
|
case errors.Is(err, data.ErrDuplicateBlindSign):
|
||||||
common.WriteJSON(w, http.StatusConflict, common.Envelope{"error": map[string]string{"message": "bereits eine Blindsignatur für diese Abstimmung angefordert"}}, nil)
|
common.WriteJSON(w, http.StatusConflict, common.Envelope{"error": map[string]string{"message": err.Error()}}, nil)
|
||||||
case errors.Is(err, data.ErrInvalidBlindedVote):
|
case errors.Is(err, data.ErrInvalidBlindedVote):
|
||||||
common.WriteJSON(w, http.StatusUnprocessableEntity, common.Envelope{"error": map[string]string{"message": err.Error()}}, nil)
|
common.WriteJSON(w, http.StatusUnprocessableEntity, common.Envelope{"error": map[string]string{"message": err.Error()}}, nil)
|
||||||
|
case errors.Is(err, data.ErrHasNotStarted):
|
||||||
|
common.WriteJSON(w, http.StatusUnprocessableEntity, common.Envelope{"error": map[string]string{"message": err.Error()}}, nil)
|
||||||
default:
|
default:
|
||||||
web.App.LogError(r, err)
|
web.App.LogError(r, err)
|
||||||
common.WriteJSON(w, http.StatusInternalServerError, common.Envelope{"error": map[string]string{"message": "internal server error"}}, nil)
|
common.WriteJSON(w, http.StatusInternalServerError, common.Envelope{"error": map[string]string{"message": "internal server error"}}, nil)
|
||||||
@@ -250,6 +253,6 @@ func (web *Web) BlindSignIssueVote(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = common.WriteJSON(w, http.StatusOK, common.Envelope{"signed": signed}, nil); err != nil {
|
if err = common.WriteJSON(w, http.StatusOK, common.Envelope{"signed": signed}, nil); err != nil {
|
||||||
web.App.LogError(r, err)
|
web.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ func (web *Web) RequireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
func (web *Web) RequirePermission(code string, next http.HandlerFunc) http.HandlerFunc {
|
func (web *Web) RequirePermission(code string, next http.HandlerFunc) http.HandlerFunc {
|
||||||
return web.RequireActivatedUser(func(w http.ResponseWriter, r *http.Request) {
|
return web.RequireActivatedUser(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if !common.GetPermissions(r).Include(code) {
|
if !common.GetPermissions(r).Include(code) {
|
||||||
web.NotPermittedResponse(w, r)
|
web.errorResponse(w, r, data.ErrNotPermitted)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
|
|||||||
+13
-20
@@ -48,20 +48,12 @@ func (web *Web) RegisterUserPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var formErrors []string
|
var formErrors []string
|
||||||
var ve *common.ValidationError
|
var customErr *data.Error
|
||||||
switch {
|
|
||||||
case errors.As(err, &ve):
|
if errors.As(err, &customErr) && len(customErr.Details) > 0 {
|
||||||
for _, msg := range ve.Errors {
|
formErrors = customErr.DetailMessages()
|
||||||
formErrors = append(formErrors, msg)
|
|
||||||
}
|
|
||||||
case errors.Is(err, data.ErrDuplicateEmail):
|
|
||||||
formErrors = []string{"Diese E-Mail-Adresse wird bereits verwendet."}
|
|
||||||
case errors.Is(err, data.ErrDuplicateUser):
|
|
||||||
formErrors = []string{"Dieser Benutzername wird bereits verwendet."}
|
|
||||||
default:
|
|
||||||
web.App.LogError(r, err)
|
|
||||||
formErrors = []string{"Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut."}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
web.render(w, r, http.StatusUnprocessableEntity, "register", struct {
|
web.render(w, r, http.StatusUnprocessableEntity, "register", struct {
|
||||||
AuthenticatedUser *data.User
|
AuthenticatedUser *data.User
|
||||||
FormErrors []string
|
FormErrors []string
|
||||||
@@ -73,8 +65,12 @@ func (web *Web) RegisterUserPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setCookie(w, authToken.Plaintext, authToken.Expiry)
|
setCookie(w, authToken.Plaintext, authToken.Expiry)
|
||||||
|
if web.App.Config.Env == "production" {
|
||||||
|
http.Redirect(w, r, "/users/activated", http.StatusSeeOther)
|
||||||
|
} else {
|
||||||
http.Redirect(w, r, "/issues", http.StatusSeeOther)
|
http.Redirect(w, r, "/issues", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (web *Web) ProfilePage(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) ProfilePage(w http.ResponseWriter, r *http.Request) {
|
||||||
user := common.GetUser(r)
|
user := common.GetUser(r)
|
||||||
@@ -83,7 +79,7 @@ func (web *Web) ProfilePage(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fullUser, err := web.App.GetUser(user.ID)
|
fullUser, err := web.App.GetUser(user, user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, data.ErrRecordNotFound) {
|
if errors.Is(err, data.ErrRecordNotFound) {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
@@ -166,19 +162,16 @@ func (web *Web) ActivateUserAction(w http.ResponseWriter, r *http.Request) {
|
|||||||
web.App.LogError(r, err)
|
web.App.LogError(r, err)
|
||||||
msg = "Aktivierung fehlgeschlagen. Bitte versuchen Sie es erneut."
|
msg = "Aktivierung fehlgeschlagen. Bitte versuchen Sie es erneut."
|
||||||
}
|
}
|
||||||
web.render(w, r, http.StatusUnprocessableEntity, "activated", struct {
|
web.renderFragment(w, r, http.StatusUnprocessableEntity, "activated", struct {
|
||||||
AuthenticatedUser *data.User
|
|
||||||
FormErrors []string
|
FormErrors []string
|
||||||
Token string
|
|
||||||
}{
|
}{
|
||||||
AuthenticatedUser: common.GetUser(r),
|
|
||||||
FormErrors: []string{msg},
|
FormErrors: []string{msg},
|
||||||
Token: token,
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
w.Header().Set("HX-Redirect", "/")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) DeleteUserAction(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) DeleteUserAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorCode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 400 variants
|
||||||
|
|
||||||
|
// 401 variants
|
||||||
|
errCodeInvalidCredentials ErrorCode = 4011
|
||||||
|
errCodeInvalidAuthToken ErrorCode = 4012
|
||||||
|
errCodeAuthRequired ErrorCode = 4013
|
||||||
|
|
||||||
|
// 403 variants
|
||||||
|
errCodeInactiveAccount ErrorCode = 4031
|
||||||
|
errCodeNotPermitted ErrorCode = 4032
|
||||||
|
|
||||||
|
// 409 variants
|
||||||
|
errCodeEditConflict ErrorCode = 4091
|
||||||
|
errCodeAlreadyVoted ErrorCode = 4092
|
||||||
|
errCodeAlreadyBlindSigned ErrorCode = 4093
|
||||||
|
errCodeVoteAlreadyCast ErrorCode = 4094
|
||||||
|
|
||||||
|
// 422 variants
|
||||||
|
errCodeValidationFailed ErrorCode = 4221
|
||||||
|
errCodeBlindedVoteRange ErrorCode = 4222
|
||||||
|
errCodeInvalidSignature ErrorCode = 4223
|
||||||
|
errCodeHasNotStarted ErrorCode = 4224
|
||||||
|
)
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
HttpCode int `json:"http_code,omitempty"`
|
||||||
|
Code ErrorCode `json:"code,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Details map[string]string `json:"details,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) WithDetails(details map[string]string) error {
|
||||||
|
e.Details = details
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (e *Error) DetailMessages() []string {
|
||||||
|
msgs := make([]string, 0, len(e.Details))
|
||||||
|
for key, msg := range e.Details {
|
||||||
|
msgs = append(msgs, fmt.Sprintf("%s: %s", key, msg))
|
||||||
|
}
|
||||||
|
return msgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(httpCode int, code ErrorCode, text string) error {
|
||||||
|
return &Error{HttpCode: httpCode, Code: code, Message: text}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// 400 Bad Request
|
||||||
|
ErrFailedPEM = New(400, 400, "failed to decode PEM block")
|
||||||
|
ErrBadlyFormedJSON = New(400, 400, "body contains badly-formed JSON")
|
||||||
|
ErrBodyEmpty = New(400, 400, "body must not be empty")
|
||||||
|
ErrSingleValue = New(400, 400, "body must only contain a single JSON value")
|
||||||
|
ErrInvalidID = New(400, 400, "invalid id parameter")
|
||||||
|
ErrBadRequest = New(400, 400, "the server cannot process the request due to a client error")
|
||||||
|
|
||||||
|
// 401 Unauthorized
|
||||||
|
ErrInvalidCredentials = New(401, errCodeInvalidCredentials, "invalid credentials")
|
||||||
|
ErrInvalidAuthToken = New(401, errCodeInvalidAuthToken, "invalid or missing authentication token")
|
||||||
|
ErrNoToken = New(401, errCodeInvalidAuthToken, "token must be provided")
|
||||||
|
ErrAuthRequired = New(401, errCodeAuthRequired, "you must be authenticated to access this resource")
|
||||||
|
|
||||||
|
// 403 Forbidden
|
||||||
|
ErrInactiveAccount = New(403, errCodeInactiveAccount, "your user account must be activated to access this resource")
|
||||||
|
ErrNotPermitted = New(403, errCodeNotPermitted, "your user account doesn't have the necessary permissions to access this resource")
|
||||||
|
ErrAuthenticatedVoteRejected = New(403, errCodeNotPermitted, "authenticated vote rejected")
|
||||||
|
|
||||||
|
// 404 Not Found
|
||||||
|
ErrRecordNotFound = New(404, 404, "record not found")
|
||||||
|
ErrNoPath = New(404, 404, "path is required")
|
||||||
|
|
||||||
|
// 409 Conflict
|
||||||
|
ErrEditConflict = New(409, errCodeEditConflict, "edit conflict")
|
||||||
|
ErrDuplicateVote = New(409, errCodeAlreadyVoted, "this signature has already been used to cast a vote")
|
||||||
|
ErrDuplicateBlindSign = New(409, errCodeAlreadyBlindSigned, "user has already requested a blind signature for this issue")
|
||||||
|
ErrDuplicateSignature = New(409, errCodeVoteAlreadyCast, "this signature has already been used to cast a vote")
|
||||||
|
ErrDuplicateEmail = New(409, 409, "duplicate email")
|
||||||
|
ErrDuplicateUser = New(409, 409, "duplicate username")
|
||||||
|
|
||||||
|
// 422 Unprocessable Entity
|
||||||
|
ErrValidationFailed = New(422, errCodeValidationFailed, "validation failed")
|
||||||
|
ErrInvalidBlindedVote = New(422, errCodeBlindedVoteRange, "blinded_vote is out of valid range [1, n-1]")
|
||||||
|
ErrInvalidSignature = New(422, errCodeInvalidSignature, "signature verification failed")
|
||||||
|
ErrHasNotStarted = New(422, errCodeHasNotStarted, "the vote has not yet started")
|
||||||
|
|
||||||
|
// 429 Too Many Requests
|
||||||
|
ErrRateLimitExceeded = New(429, 429, "rate limit exceeded")
|
||||||
|
)
|
||||||
@@ -3,14 +3,13 @@ package data
|
|||||||
import(
|
import(
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parsePrivateKey(pemBytes []byte) (*rsa.PrivateKey, error) {
|
func parsePrivateKey(pemBytes []byte) (*rsa.PrivateKey, error) {
|
||||||
block, _ := pem.Decode(pemBytes)
|
block, _ := pem.Decode(pemBytes)
|
||||||
if block == nil {
|
if block == nil {
|
||||||
return nil, errors.New("failed to decode PEM block")
|
return nil, ErrFailedPEM
|
||||||
}
|
}
|
||||||
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,6 @@ package data
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrRecordNotFound = errors.New("record not found")
|
|
||||||
ErrEditConflict = errors.New("edit conflict")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Models struct {
|
type Models struct {
|
||||||
|
|||||||
+34
-21
@@ -12,26 +12,20 @@ import (
|
|||||||
"party.at/party/internal/validator"
|
"party.at/party/internal/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
ErrDuplicateEmail = errors.New("duplicate email")
|
|
||||||
ErrDuplicateUser = errors.New("duplicate username")
|
|
||||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
|
||||||
)
|
|
||||||
|
|
||||||
var AnonymousUser = &User{}
|
var AnonymousUser = &User{}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id,omitempty"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email,omitempty"`
|
||||||
PhoneNumber string `json:"phone_number"`
|
PhoneNumber string `json:"phone_number,omitempty"`
|
||||||
Country string `json:"country"`
|
Country string `json:"country,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name,omitempty"`
|
||||||
AltName *string `json:"alt_name"`
|
AltName *string `json:"alt_name,omitempty"`
|
||||||
DateOfBirth time.Time `json:"date_of_birth"`
|
DateOfBirth time.Time `json:"date_of_birth,omitempty"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address,omitempty"`
|
||||||
Created time.Time `json:"created"`
|
Created time.Time `json:"created,omitempty"`
|
||||||
LastLogin time.Time `json:"last_login"`
|
LastLogin time.Time `json:"last_login,omitempty"`
|
||||||
Activated bool `json:"activated"`
|
Activated bool `json:"activated,omitempty"`
|
||||||
Version int32 `json:"-"`
|
Version int32 `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,10 +306,29 @@ func (m UserModel) Update(user *User) error {
|
|||||||
|
|
||||||
func (m UserModel) GetAll(filters Filters) ([]*User, Metadata, error) {
|
func (m UserModel) GetAll(filters Filters) ([]*User, Metadata, error) {
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
SELECT COUNT(*) OVER(), id, email, phone_number, country, name, alt_name, date_of_birth, address, created, last_login, activated, version
|
SELECT
|
||||||
FROM users
|
COUNT(*) OVER(),
|
||||||
ORDER BY %s %s, id ASC
|
id,
|
||||||
LIMIT $1 OFFSET $2`,
|
email,
|
||||||
|
phone_number,
|
||||||
|
country,
|
||||||
|
name,
|
||||||
|
alt_name,
|
||||||
|
date_of_birth,
|
||||||
|
address,
|
||||||
|
created,
|
||||||
|
last_login,
|
||||||
|
activated,
|
||||||
|
version
|
||||||
|
FROM
|
||||||
|
users
|
||||||
|
ORDER BY
|
||||||
|
%s %s,
|
||||||
|
id ASC
|
||||||
|
LIMIT
|
||||||
|
$1
|
||||||
|
OFFSET
|
||||||
|
$2`,
|
||||||
filters.sortColumn(),
|
filters.sortColumn(),
|
||||||
filters.sortDirection(),
|
filters.sortDirection(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package data
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"time"
|
"time"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"math/big"
|
"math/big"
|
||||||
@@ -12,14 +11,6 @@ import (
|
|||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
ErrDuplicateBlindSign = errors.New("user has already requested a blind signature for this issue")
|
|
||||||
ErrInvalidBlindedVote = errors.New("blinded_vote is out of valid range [1, n-1]")
|
|
||||||
ErrInvalidSignature = errors.New("signature verification failed")
|
|
||||||
ErrDuplicateVote = errors.New("this signature has already been used to cast a vote")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
type Vote struct {
|
type Vote struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
OptionID int64 `json:"option_id"`
|
OptionID int64 `json:"option_id"`
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -67,7 +66,6 @@ func (l *Logger) print(level Level, message string, properties map[string]string
|
|||||||
Time string `json:"time"`
|
Time string `json:"time"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Properties map[string]string `json:"properties,omitempty"`
|
Properties map[string]string `json:"properties,omitempty"`
|
||||||
Trace string `json:"trace,omitempty"`
|
|
||||||
}{
|
}{
|
||||||
Level: level.String(),
|
Level: level.String(),
|
||||||
Time: time.Now().UTC().Format(time.RFC3339),
|
Time: time.Now().UTC().Format(time.RFC3339),
|
||||||
@@ -75,10 +73,6 @@ func (l *Logger) print(level Level, message string, properties map[string]string
|
|||||||
Properties: properties,
|
Properties: properties,
|
||||||
}
|
}
|
||||||
|
|
||||||
if level >= LevelError {
|
|
||||||
aux.Trace = string(debug.Stack())
|
|
||||||
}
|
|
||||||
|
|
||||||
var line []byte
|
var line []byte
|
||||||
|
|
||||||
line, err := json.Marshal(aux)
|
line, err := json.Marshal(aux)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{{define "activated"}}
|
||||||
|
<div class="alert alert--error">
|
||||||
|
{{range .FormErrors}}<p>{{.}}</p>{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
@@ -8,13 +8,9 @@
|
|||||||
<h2 class="auth-title">Konto aktivieren</h2>
|
<h2 class="auth-title">Konto aktivieren</h2>
|
||||||
<p class="auth-subtitle">Geben Sie Ihren Aktivierungstoken ein, um Ihr Konto freizuschalten.</p>
|
<p class="auth-subtitle">Geben Sie Ihren Aktivierungstoken ein, um Ihr Konto freizuschalten.</p>
|
||||||
|
|
||||||
{{if .FormErrors}}
|
<div id="activation-result"></div>
|
||||||
<div class="alert alert--error">
|
|
||||||
{{range .FormErrors}}<p>{{.}}</p>{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<form method="POST" action="/users/activated">
|
<form hx-put="/users/activated" hx-target="#activation-result" hx-swap="innerHTML">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="token">Aktivierungstoken</label>
|
<label class="form-label" for="token">Aktivierungstoken</label>
|
||||||
<input class="form-input" id="token" name="token" type="text"
|
<input class="form-input" id="token" name="token" type="text"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<body>
|
<body>
|
||||||
{{if .AuthenticatedUser}}
|
{{if .AuthenticatedUser}}
|
||||||
<nav style="border-bottom:1px solid var(--border); background:var(--bg-card);">
|
<nav style="border-bottom:1px solid var(--border); background:var(--bg-card);">
|
||||||
<div class="container" style="display:flex; align-items:center; gap:20px; padding:10px 0;">
|
<div class="container" style="display:flex; align-items:center; gap:10px 20px; padding-top:10px; padding-bottom:10px; flex-wrap:wrap;">
|
||||||
<a href="/" class="link">Start</a>
|
<a href="/" class="link">Start</a>
|
||||||
<a href="/issues" class="link">Abstimmungen</a>
|
<a href="/issues" class="link">Abstimmungen</a>
|
||||||
<a href="/mps" class="link">Nationalrat</a>
|
<a href="/mps" class="link">Nationalrat</a>
|
||||||
@@ -27,10 +27,6 @@
|
|||||||
</nav>
|
</nav>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div style="text-align:center; padding: 40px 0 24px;">
|
|
||||||
<img src="/static/logo.svg" alt="DPÖ Logo" style="height: 600px; width: auto;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main class="site-main">
|
<main class="site-main">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{{template "body" .}}
|
{{template "body" .}}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
{{define "title"}}Übersicht{{end}}
|
{{define "title"}}Übersicht{{end}}
|
||||||
|
|
||||||
{{define "body"}}
|
{{define "body"}}
|
||||||
|
<div style="text-align:center; padding: 40px 0 24px;">
|
||||||
|
<img src="/static/logo.svg" alt="DPÖ Logo" style="height: auto; max-height: 600px; width: 100%; max-width: 600px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1 class="page-title">Willkommen, {{.Name}}</h1>
|
<h1 class="page-title">Willkommen, {{.Name}}</h1>
|
||||||
<p class="page-subtitle">Ihre demokratische Plattform für Österreich</p>
|
<p class="page-subtitle">Ihre demokratische Plattform für Österreich</p>
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
{{define "title"}}Willkommen{{end}}
|
{{define "title"}}Willkommen{{end}}
|
||||||
|
|
||||||
{{define "body"}}
|
{{define "body"}}
|
||||||
|
<div style="text-align:center; padding: 40px 0 24px;">
|
||||||
|
<img src="/static/logo.svg" alt="DPÖ Logo" style="height: auto; max-height: 600px; width: 100%; max-width: 600px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<div class="hero-label">Digitale Demokratie für Österreich</div>
|
<div class="hero-label">Digitale Demokratie für Österreich</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
{{.Name}}
|
{{.Name}}
|
||||||
<span style="font-size: 13px; font-weight: 500; color: var(--text-muted); font-family: 'Source Sans 3', sans-serif;">{{len .Members}} Mandate</span>
|
<span style="font-size: 13px; font-weight: 500; color: var(--text-muted); font-family: 'Source Sans 3', sans-serif;">{{len .Members}} Mandate</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div style="background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow);">
|
<div style="background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow);" class="table-scroll">
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
|
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style="background: var(--bg); border-bottom: 1px solid var(--border);">
|
<tr style="background: var(--bg); border-bottom: 1px solid var(--border);">
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{/* ── Two-column faction breakdown ─────────────────────────────────────── */}}
|
{{/* ── Two-column faction breakdown ─────────────────────────────────────── */}}
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px;">
|
<div class="faction-grid">
|
||||||
|
|
||||||
{{/* Yes column */}}
|
{{/* Yes column */}}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<span>{{.Label}}</span>
|
<span>{{.Label}}</span>
|
||||||
<span style="font-size: 12px; font-weight: 400; color: var(--text-muted);">{{len .Docs}}</span>
|
<span style="font-size: 12px; font-weight: 400; color: var(--text-muted);">{{len .Docs}}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div style="border: 1px solid var(--border); border-top: none; border-radius: 0 0 var(--radius) var(--radius); overflow: hidden; background: var(--bg-card);">
|
<div style="border: 1px solid var(--border); border-top: none; border-radius: 0 0 var(--radius) var(--radius); background: var(--bg-card);" class="table-scroll">
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
|
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style="background: var(--bg); border-bottom: 1px solid var(--border);">
|
<tr style="background: var(--bg); border-bottom: 1px solid var(--border);">
|
||||||
|
|||||||
+46
-12
@@ -3,25 +3,59 @@
|
|||||||
{{define "title"}}Mein Profil{{end}}
|
{{define "title"}}Mein Profil{{end}}
|
||||||
|
|
||||||
{{define "body"}}
|
{{define "body"}}
|
||||||
<div class="page-header">
|
<div style="display:flex; flex-direction:column; align-items:center; padding-top:32px;">
|
||||||
<h1 class="page-title">Mein Profil</h1>
|
|
||||||
|
<div style="width:88px; height:88px; border-radius:50%; background:var(--accent); display:flex; align-items:center; justify-content:center; font-family:'Montserrat',sans-serif; font-size:34px; font-weight:700; color:#1a1008; box-shadow:0 4px 18px rgba(200,150,26,0.35); margin-bottom:20px; flex-shrink:0;">
|
||||||
|
{{slice .User.Name 0 1}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="max-width:520px;">
|
<h1 style="font-family:'Montserrat',sans-serif; font-size:28px; font-weight:700; color:var(--dark); text-align:center; margin-bottom:4px;">
|
||||||
<div class="card-title">{{.User.Name}}{{if .User.AltName}} / {{.User.AltName}}{{end}}</div>
|
{{.User.Name}}{{if .User.AltName}} <span style="color:var(--text-muted); font-weight:600;">/ {{.User.AltName}}</span>{{end}}
|
||||||
<div style="margin-top:12px; display:flex; flex-direction:column; gap:8px; font-size:15px;">
|
</h1>
|
||||||
<div><span style="color:var(--text-muted);">E-Mail</span> {{.User.Email}}</div>
|
<p style="font-size:13px; font-weight:600; letter-spacing:0.09em; text-transform:uppercase; color:var(--accent); margin-bottom:36px;">Mitglied</p>
|
||||||
<div><span style="color:var(--text-muted);">Telefon</span> {{.User.PhoneNumber}}</div>
|
|
||||||
<div><span style="color:var(--text-muted);">Land</span> {{.User.Country}}</div>
|
<div style="background:var(--bg-card); border:1px solid var(--border); border-radius:12px; box-shadow:0 6px 32px rgba(0,0,0,0.10); width:100%; max-width:620px; overflow:hidden;">
|
||||||
<div><span style="color:var(--text-muted);">Adresse</span> {{.User.Address}}</div>
|
|
||||||
<div><span style="color:var(--text-muted);">Geburtsdatum</span> {{.User.DateOfBirth.Format "02.01.2006"}}</div>
|
<div style="background:var(--dark); padding:16px 32px; border-bottom:3px solid var(--accent);">
|
||||||
<div><span style="color:var(--text-muted);">Mitglied seit</span> {{.User.Created.Format "02.01.2006"}}</div>
|
<span style="font-family:'Montserrat',sans-serif; font-size:12px; font-weight:700; letter-spacing:0.1em; text-transform:uppercase; color:rgba(255,255,255,0.5);">Profildaten</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top:20px; border-top:1px solid var(--border); padding-top:16px;">
|
<div style="padding:0 32px;">
|
||||||
|
<div style="display:flex; align-items:baseline; gap:16px; padding:18px 0; border-bottom:1px solid var(--border);">
|
||||||
|
<span style="font-size:13px; font-weight:600; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.06em; width:140px; flex-shrink:0;">E-Mail</span>
|
||||||
|
<span style="font-size:16px; color:var(--text);">{{.User.Email}}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:baseline; gap:16px; padding:18px 0; border-bottom:1px solid var(--border);">
|
||||||
|
<span style="font-size:13px; font-weight:600; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.06em; width:140px; flex-shrink:0;">Telefon</span>
|
||||||
|
<span style="font-size:16px; color:var(--text);">{{.User.PhoneNumber}}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:baseline; gap:16px; padding:18px 0; border-bottom:1px solid var(--border);">
|
||||||
|
<span style="font-size:13px; font-weight:600; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.06em; width:140px; flex-shrink:0;">Land</span>
|
||||||
|
<span style="font-size:16px; color:var(--text);">{{.User.Country}}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:baseline; gap:16px; padding:18px 0; border-bottom:1px solid var(--border);">
|
||||||
|
<span style="font-size:13px; font-weight:600; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.06em; width:140px; flex-shrink:0;">Adresse</span>
|
||||||
|
<span style="font-size:16px; color:var(--text);">{{.User.Address}}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:baseline; gap:16px; padding:18px 0; border-bottom:1px solid var(--border);">
|
||||||
|
<span style="font-size:13px; font-weight:600; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.06em; width:140px; flex-shrink:0;">Geburtsdatum</span>
|
||||||
|
<span style="font-size:16px; color:var(--text);">{{.User.DateOfBirth.Format "02.01.2006"}}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:baseline; gap:16px; padding:18px 0;">
|
||||||
|
<span style="font-size:13px; font-weight:600; color:var(--text-muted); text-transform:uppercase; letter-spacing:0.06em; width:140px; flex-shrink:0;">Mitglied seit</span>
|
||||||
|
<span style="font-size:16px; color:var(--accent); font-weight:600;">{{.User.Created.Format "02.01.2006"}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="border-top:1px solid var(--border); background:#fef2f2; padding:20px 32px; display:flex; align-items:center; justify-content:space-between;">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:13px; font-weight:700; color:#991b1b; text-transform:uppercase; letter-spacing:0.06em; margin-bottom:2px;">Gefahrenzone</div>
|
||||||
|
<div style="font-size:13px; color:#b91c1c;">Diese Aktion kann nicht rückgängig gemacht werden.</div>
|
||||||
|
</div>
|
||||||
<button class="btn btn--danger"
|
<button class="btn btn--danger"
|
||||||
hx-delete="/users/{{.User.ID}}"
|
hx-delete="/users/{{.User.ID}}"
|
||||||
hx-confirm="Konto wirklich unwiderruflich löschen?">Konto löschen</button>
|
hx-confirm="Konto wirklich unwiderruflich löschen?">Konto löschen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<form method="POST" action="/register">
|
<form method="POST" action="/register">
|
||||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap: 0 16px;">
|
<div class="form-row-2">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="name">Vollständiger Name</label>
|
<label class="form-label" for="name">Vollständiger Name</label>
|
||||||
<input class="form-input" type="text" id="name" name="name"
|
<input class="form-input" type="text" id="name" name="name"
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
placeholder="••••••••" required autocomplete="new-password">
|
placeholder="••••••••" required autocomplete="new-password">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap: 0 16px;">
|
<div class="form-row-2">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="phone_number">Telefonnummer</label>
|
<label class="form-label" for="phone_number">Telefonnummer</label>
|
||||||
<input class="form-input" type="tel" id="phone_number" name="phone_number"
|
<input class="form-input" type="tel" id="phone_number" name="phone_number"
|
||||||
|
|||||||
@@ -373,6 +373,17 @@ a:hover {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn--danger {
|
||||||
|
background: #dc2626;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--danger:hover {
|
||||||
|
background: #b91c1c;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(220,38,38,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.btn--full {
|
.btn--full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
@@ -430,11 +441,40 @@ a:hover {
|
|||||||
color: rgba(255,255,255,0.2);
|
color: rgba(255,255,255,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Table scroll wrapper ── */
|
||||||
|
|
||||||
|
.table-scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 2-col form row ── */
|
||||||
|
|
||||||
|
.form-row-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Faction breakdown grid ── */
|
||||||
|
|
||||||
|
.faction-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Responsive ── */
|
/* ── Responsive ── */
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.brand-name { display: none; }
|
.brand-name { display: none; }
|
||||||
.hero-title { font-size: 36px; }
|
.hero-title { font-size: 36px; }
|
||||||
|
.hero { padding: 40px 0 32px; margin-bottom: 32px; }
|
||||||
.auth-card { padding: 24px; }
|
.auth-card { padding: 24px; }
|
||||||
.card-grid { grid-template-columns: 1fr; }
|
.card-grid { grid-template-columns: 1fr; }
|
||||||
|
.form-row-2 { grid-template-columns: 1fr; }
|
||||||
|
.faction-grid { grid-template-columns: 1fr; }
|
||||||
|
.site-main { padding: 28px 0 40px; }
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user