.
This commit is contained in:
parent
90cff1ac5b
commit
6f6eea5529
@ -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"
|
||||||
@ -41,38 +40,14 @@ func (api *Api) ListUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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.errorResponse(w, r, data.ErrBadRequest)
|
api.errorResponse(w, r, data.ErrBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user, authToken, err := api.App.RegisterUser(input)
|
||||||
|
|
||||||
user, authToken, err := api.App.RegisterUser(common.RegisterUserInput{
|
|
||||||
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 {
|
||||||
api.errorResponse(w, r, err)
|
api.errorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
@ -84,9 +59,11 @@ func (api *Api) CreateUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@ -98,12 +75,7 @@ func (api *Api) ReadUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if common.GetUser(r).ID != id {
|
target, err := api.App.GetUser(user, id)
|
||||||
api.errorResponse(w, r, data.ErrNotPermitted)
|
|
||||||
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.errorResponse(w, r, data.ErrRecordNotFound)
|
api.errorResponse(w, r, data.ErrRecordNotFound)
|
||||||
@ -113,7 +85,7 @@ func (api *Api) ReadUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
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.ServerErrorResponse(w, r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
@ -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 = "admin"
|
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) {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
27
cmd/party/users_test.go
Normal file
27
cmd/party/users_test.go
Normal file
@ -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", 200, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Contains(body, []byte("An old silent pond...")) {
|
||||||
|
t.Errorf("want body to contain %q; got %q", "", string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -75,7 +75,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)
|
||||||
|
|||||||
@ -7,26 +7,28 @@ import (
|
|||||||
type ErrorCode int
|
type ErrorCode int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// 400 variants
|
||||||
|
|
||||||
// 401 variants
|
// 401 variants
|
||||||
ErrCodeInvalidCredentials ErrorCode = 4011
|
errCodeInvalidCredentials ErrorCode = 4011
|
||||||
ErrCodeInvalidAuthToken ErrorCode = 4012
|
errCodeInvalidAuthToken ErrorCode = 4012
|
||||||
ErrCodeAuthRequired ErrorCode = 4013
|
errCodeAuthRequired ErrorCode = 4013
|
||||||
|
|
||||||
// 403 variants
|
// 403 variants
|
||||||
ErrCodeInactiveAccount ErrorCode = 4031
|
errCodeInactiveAccount ErrorCode = 4031
|
||||||
ErrCodeNotPermitted ErrorCode = 4032
|
errCodeNotPermitted ErrorCode = 4032
|
||||||
|
|
||||||
// 409 variants
|
// 409 variants
|
||||||
ErrCodeEditConflict ErrorCode = 4091
|
errCodeEditConflict ErrorCode = 4091
|
||||||
ErrCodeAlreadyVoted ErrorCode = 4092
|
errCodeAlreadyVoted ErrorCode = 4092
|
||||||
ErrCodeAlreadyBlindSigned ErrorCode = 4093
|
errCodeAlreadyBlindSigned ErrorCode = 4093
|
||||||
ErrCodeVoteAlreadyCast ErrorCode = 4094
|
errCodeVoteAlreadyCast ErrorCode = 4094
|
||||||
|
|
||||||
// 422 variants
|
// 422 variants
|
||||||
ErrCodeValidationFailed ErrorCode = 4221
|
errCodeValidationFailed ErrorCode = 4221
|
||||||
ErrCodeBlindedVoteRange ErrorCode = 4222
|
errCodeBlindedVoteRange ErrorCode = 4222
|
||||||
ErrCodeInvalidSignature ErrorCode = 4223
|
errCodeInvalidSignature ErrorCode = 4223
|
||||||
ErrCodeHasNotStarted ErrorCode = 4224
|
errCodeHasNotStarted ErrorCode = 4224
|
||||||
)
|
)
|
||||||
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
@ -60,41 +62,41 @@ func New(httpCode int, code ErrorCode, text string) error {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// 400 Bad Request
|
// 400 Bad Request
|
||||||
ErrFailedPEM = New(400, 0, "failed to decode PEM block")
|
ErrFailedPEM = New(400, 400, "failed to decode PEM block")
|
||||||
ErrBadlyFormedJSON = New(400, 0, "body contains badly-formed JSON")
|
ErrBadlyFormedJSON = New(400, 400, "body contains badly-formed JSON")
|
||||||
ErrBodyEmpty = New(400, 0, "body must not be empty")
|
ErrBodyEmpty = New(400, 400, "body must not be empty")
|
||||||
ErrSingleValue = New(400, 0, "body must only contain a single JSON value")
|
ErrSingleValue = New(400, 400, "body must only contain a single JSON value")
|
||||||
ErrInvalidID = New(400, 0, "invalid id parameter")
|
ErrInvalidID = New(400, 400, "invalid id parameter")
|
||||||
ErrBadRequest = New(400, 0, "the server cannot process the request due to a client error")
|
ErrBadRequest = New(400, 400, "the server cannot process the request due to a client error")
|
||||||
|
|
||||||
// 401 Unauthorized
|
// 401 Unauthorized
|
||||||
ErrInvalidCredentials = New(401, 4011, "invalid credentials")
|
ErrInvalidCredentials = New(401, errCodeInvalidCredentials, "invalid credentials")
|
||||||
ErrInvalidAuthToken = New(401, 4012, "invalid or missing authentication token")
|
ErrInvalidAuthToken = New(401, errCodeInvalidAuthToken, "invalid or missing authentication token")
|
||||||
ErrNoToken = New(401, 4012, "token must be provided")
|
ErrNoToken = New(401, errCodeInvalidAuthToken, "token must be provided")
|
||||||
ErrAuthRequired = New(401, 4013, "you must be authenticated to access this resource")
|
ErrAuthRequired = New(401, errCodeAuthRequired, "you must be authenticated to access this resource")
|
||||||
ErrHasNotStarted = New(401, 4224, "the vote has not yet started")
|
|
||||||
|
|
||||||
// 403 Forbidden
|
// 403 Forbidden
|
||||||
ErrInactiveAccount = New(403, 4031, "your user account must be activated to access this resource")
|
ErrInactiveAccount = New(403, errCodeInactiveAccount, "your user account must be activated to access this resource")
|
||||||
ErrNotPermitted = New(403, 4032, "your user account doesn't have the necessary permissions to access this resource")
|
ErrNotPermitted = New(403, errCodeNotPermitted, "your user account doesn't have the necessary permissions to access this resource")
|
||||||
|
|
||||||
// 404 Not Found
|
// 404 Not Found
|
||||||
ErrRecordNotFound = New(404, 0, "record not found")
|
ErrRecordNotFound = New(404, 404, "record not found")
|
||||||
ErrNoPath = New(404, 0, "path is required")
|
ErrNoPath = New(404, 404, "path is required")
|
||||||
|
|
||||||
// 409 Conflict
|
// 409 Conflict
|
||||||
ErrEditConflict = New(409, 4091, "edit conflict")
|
ErrEditConflict = New(409, errCodeEditConflict, "edit conflict")
|
||||||
ErrDuplicateVote = New(409, 4092, "this signature has already been used to cast a vote")
|
ErrDuplicateVote = New(409, errCodeAlreadyVoted, "this signature has already been used to cast a vote")
|
||||||
ErrDuplicateBlindSign = New(409, 4093, "user has already requested a blind signature for this issue")
|
ErrDuplicateBlindSign = New(409, errCodeAlreadyBlindSigned, "user has already requested a blind signature for this issue")
|
||||||
ErrDuplicateSignature = New(409, 4094, "this signature has already been used to cast a vote")
|
ErrDuplicateSignature = New(409, errCodeVoteAlreadyCast, "this signature has already been used to cast a vote")
|
||||||
ErrDuplicateEmail = New(409, 0, "duplicate email")
|
ErrDuplicateEmail = New(409, 409, "duplicate email")
|
||||||
ErrDuplicateUser = New(409, 0, "duplicate username")
|
ErrDuplicateUser = New(409, 409, "duplicate username")
|
||||||
|
|
||||||
// 422 Unprocessable Entity
|
// 422 Unprocessable Entity
|
||||||
ErrValidationFailed = New(422, 4221, "validation failed")
|
ErrValidationFailed = New(422, errCodeValidationFailed, "validation failed")
|
||||||
ErrInvalidBlindedVote = New(422, 4222, "blinded_vote is out of valid range [1, n-1]")
|
ErrInvalidBlindedVote = New(422, errCodeBlindedVoteRange, "blinded_vote is out of valid range [1, n-1]")
|
||||||
ErrInvalidSignature = New(422, 4223, "signature verification failed")
|
ErrInvalidSignature = New(422, errCodeInvalidSignature, "signature verification failed")
|
||||||
|
ErrHasNotStarted = New(422, errCodeHasNotStarted, "the vote has not yet started")
|
||||||
|
|
||||||
// 429 Too Many Requests
|
// 429 Too Many Requests
|
||||||
ErrRateLimitExceeded = New(429, 0, "rate limit exceeded")
|
ErrRateLimitExceeded = New(429, 429, "rate limit exceeded")
|
||||||
)
|
)
|
||||||
|
|||||||
@ -15,17 +15,17 @@ import (
|
|||||||
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:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,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(),
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user