Initial commit

This commit is contained in:
Hayden 2022-08-29 18:30:36 -08:00
commit 29f583e936
135 changed files with 18463 additions and 0 deletions

View file

@ -0,0 +1,29 @@
package v1
import (
"github.com/hay-kot/git-web-template/backend/internal/services"
"github.com/hay-kot/git-web-template/backend/pkgs/logger"
)
type V1Controller struct {
log *logger.Logger
svc *services.AllServices
}
func BaseUrlFunc(prefix string) func(s string) string {
v1Base := prefix + "/v1"
prefixFunc := func(s string) string {
return v1Base + s
}
return prefixFunc
}
func NewControllerV1(log *logger.Logger, svc *services.AllServices) *V1Controller {
ctrl := &V1Controller{
log: log,
svc: svc,
}
return ctrl
}

View file

@ -0,0 +1,20 @@
package v1
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_NewHandlerV1(t *testing.T) {
v1Base := BaseUrlFunc("/testing/v1")
ctrl := NewControllerV1(mockHandler.log, mockHandler.svc)
assert.NotNil(t, ctrl)
assert.Equal(t, ctrl.log, mockHandler.log)
assert.Equal(t, "/testing/v1/v1/abc123", v1Base("/abc123"))
assert.Equal(t, "/testing/v1/v1/abc123", v1Base("/abc123"))
}

View file

@ -0,0 +1,51 @@
package v1
import (
"context"
"testing"
"github.com/hay-kot/git-web-template/backend/internal/mocks"
"github.com/hay-kot/git-web-template/backend/internal/mocks/factories"
"github.com/hay-kot/git-web-template/backend/internal/types"
)
var mockHandler = &V1Controller{}
var users = []types.UserOut{}
func userPool() func() {
create := []types.UserCreate{
factories.UserFactory(),
factories.UserFactory(),
factories.UserFactory(),
factories.UserFactory(),
}
userOut := []types.UserOut{}
for _, user := range create {
usrOut, _ := mockHandler.svc.Admin.Create(context.Background(), user)
userOut = append(userOut, usrOut)
}
users = userOut
purge := func() {
mockHandler.svc.Admin.DeleteAll(context.Background())
}
return purge
}
func TestMain(m *testing.M) {
// Set Handler Vars
mockHandler.log = mocks.GetStructLogger()
repos, closeDb := mocks.GetEntRepos()
mockHandler.svc = mocks.GetMockServices(repos)
defer closeDb()
purge := userPool()
defer purge()
m.Run()
}

View file

@ -0,0 +1,207 @@
package v1
import (
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/hay-kot/git-web-template/backend/internal/services"
"github.com/hay-kot/git-web-template/backend/internal/types"
"github.com/hay-kot/git-web-template/backend/pkgs/hasher"
"github.com/hay-kot/git-web-template/backend/pkgs/logger"
"github.com/hay-kot/git-web-template/backend/pkgs/server"
)
// HandleAdminUserGetAll godoc
// @Summary Gets all users from the database
// @Tags Admin: Users
// @Produce json
// @Success 200 {object} server.Result{item=[]types.UserOut}
// @Router /v1/admin/users [get]
// @Security Bearer
func (ctrl *V1Controller) HandleAdminUserGetAll() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
users, err := ctrl.svc.Admin.GetAll(r.Context())
if err != nil {
server.RespondError(w, http.StatusInternalServerError, err)
return
}
server.Respond(w, http.StatusOK, server.Wrap(users))
}
}
// HandleAdminUserGet godoc
// @Summary Get a user from the database
// @Tags Admin: Users
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {object} server.Result{item=types.UserOut}
// @Router /v1/admin/users/{id} [get]
// @Security Bearer
func (ctrl *V1Controller) HandleAdminUserGet() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
uid, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
ctrl.log.Debug(err.Error(), logger.Props{
"scope": "admin",
"details": "failed to convert id to valid UUID",
})
server.RespondError(w, http.StatusBadRequest, err)
return
}
user, err := ctrl.svc.Admin.GetByID(r.Context(), uid)
if err != nil {
ctrl.log.Error(err, nil)
server.RespondError(w, http.StatusInternalServerError, err)
return
}
server.Respond(w, http.StatusOK, server.Wrap(user))
}
}
// HandleAdminUserCreate godoc
// @Summary Create a new user
// @Tags Admin: Users
// @Produce json
// @Param payload body types.UserCreate true "User Data"
// @Success 200 {object} server.Result{item=types.UserOut}
// @Router /v1/admin/users [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleAdminUserCreate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
createData := types.UserCreate{}
if err := server.Decode(r, &createData); err != nil {
ctrl.log.Error(err, logger.Props{
"scope": "admin",
"details": "failed to decode user create data",
})
server.RespondError(w, http.StatusBadRequest, err)
return
}
err := createData.Validate()
if err != nil {
server.RespondError(w, http.StatusUnprocessableEntity, err)
return
}
hashedPw, err := hasher.HashPassword(createData.Password)
if err != nil {
ctrl.log.Error(err, logger.Props{
"scope": "admin",
"details": "failed to hash password",
})
server.RespondError(w, http.StatusInternalServerError, err)
return
}
createData.Password = hashedPw
userOut, err := ctrl.svc.Admin.Create(r.Context(), createData)
if err != nil {
ctrl.log.Error(err, logger.Props{
"scope": "admin",
"details": "failed to create user",
})
server.RespondError(w, http.StatusInternalServerError, err)
return
}
server.Respond(w, http.StatusCreated, server.Wrap(userOut))
}
}
// HandleAdminUserUpdate godoc
// @Summary Update a User
// @Tags Admin: Users
// @Param id path string true "User ID"
// @Param payload body types.UserUpdate true "User Data"
// @Produce json
// @Success 200 {object} server.Result{item=types.UserOut}
// @Router /v1/admin/users/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleAdminUserUpdate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
uid, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
ctrl.log.Debug(err.Error(), logger.Props{
"scope": "admin",
"details": "failed to convert id to valid UUID",
})
}
updateData := types.UserUpdate{}
if err := server.Decode(r, &updateData); err != nil {
ctrl.log.Error(err, logger.Props{
"scope": "admin",
"details": "failed to decode user update data",
})
server.RespondError(w, http.StatusBadRequest, err)
return
}
newData, err := ctrl.svc.Admin.UpdateProperties(r.Context(), uid, updateData)
if err != nil {
ctrl.log.Error(err, logger.Props{
"scope": "admin",
"details": "failed to update user",
})
server.RespondError(w, http.StatusInternalServerError, err)
return
}
server.Respond(w, http.StatusOK, server.Wrap(newData))
}
}
// HandleAdminUserDelete godoc
// @Summary Delete a User
// @Tags Admin: Users
// @Param id path string true "User ID"
// @Produce json
// @Success 204
// @Router /v1/admin/users/{id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleAdminUserDelete() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
uid, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
ctrl.log.Debug(err.Error(), logger.Props{
"scope": "admin",
"details": "failed to convert id to valid UUID",
})
}
actor := services.UseUserCtx(r.Context())
if actor.ID == uid {
server.RespondError(w, http.StatusBadRequest, errors.New("cannot delete yourself"))
return
}
err = ctrl.svc.Admin.Delete(r.Context(), uid)
if err != nil {
ctrl.log.Error(err, logger.Props{
"scope": "admin",
"details": "failed to delete user",
})
server.RespondError(w, http.StatusInternalServerError, err)
return
}
}
}

View file

@ -0,0 +1,109 @@
package v1
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/hay-kot/git-web-template/backend/internal/mocks/chimocker"
"github.com/hay-kot/git-web-template/backend/internal/mocks/factories"
"github.com/hay-kot/git-web-template/backend/internal/types"
"github.com/hay-kot/git-web-template/backend/pkgs/server"
"github.com/stretchr/testify/assert"
)
const (
UrlUser = "/api/v1/admin/users"
UrlUserId = "/api/v1/admin/users/%v"
UrlUserIdChi = "/api/v1/admin/users/{id}"
)
type usersResponse struct {
Users []types.UserOut `json:"item"`
}
type userResponse struct {
User types.UserOut `json:"item"`
}
func Test_HandleAdminUserGetAll_Success(t *testing.T) {
r := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, UrlUser, nil)
mockHandler.HandleAdminUserGetAll()(r, req)
response := usersResponse{
Users: []types.UserOut{},
}
_ = json.Unmarshal(r.Body.Bytes(), &response)
assert.Equal(t, http.StatusOK, r.Code)
assert.Equal(t, len(users), len(response.Users))
knowEmail := []string{
users[0].Email,
users[1].Email,
users[2].Email,
users[3].Email,
}
for _, user := range users {
assert.Contains(t, knowEmail, user.Email)
}
}
func Test_HandleAdminUserGet_Success(t *testing.T) {
targetUser := users[2]
res := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf(UrlUserId, targetUser.ID), nil)
req = chimocker.WithUrlParam(req, "id", fmt.Sprintf("%v", targetUser.ID))
mockHandler.HandleAdminUserGet()(res, req)
assert.Equal(t, http.StatusOK, res.Code)
response := userResponse{
User: types.UserOut{},
}
_ = json.Unmarshal(res.Body.Bytes(), &response)
assert.Equal(t, targetUser.ID, response.User.ID)
}
func Test_HandleAdminUserCreate_Success(t *testing.T) {
payload := factories.UserFactory()
r := httptest.NewRecorder()
body, err := json.Marshal(payload)
assert.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, UrlUser, bytes.NewBuffer(body))
req.Header.Set(server.ContentType, server.ContentJSON)
mockHandler.HandleAdminUserCreate()(r, req)
assert.Equal(t, http.StatusCreated, r.Code)
usr, err := mockHandler.svc.Admin.GetByEmail(context.Background(), payload.Email)
assert.NoError(t, err)
assert.Equal(t, payload.Email, usr.Email)
assert.Equal(t, payload.Name, usr.Name)
assert.NotEqual(t, payload.Password, usr.Password) // smoke test - check password is hashed
_ = mockHandler.svc.Admin.Delete(context.Background(), usr.ID)
}
func Test_HandleAdminUserUpdate_Success(t *testing.T) {
t.Skip()
}
func Test_HandleAdminUserUpdate_Delete(t *testing.T) {
t.Skip()
}

View file

@ -0,0 +1,136 @@
package v1
import (
"errors"
"net/http"
"github.com/hay-kot/git-web-template/backend/internal/services"
"github.com/hay-kot/git-web-template/backend/internal/types"
"github.com/hay-kot/git-web-template/backend/pkgs/logger"
"github.com/hay-kot/git-web-template/backend/pkgs/server"
)
var (
HeaderFormData = "application/x-www-form-urlencoded"
HeaderJSON = "application/json"
)
// HandleAuthLogin godoc
// @Summary User Login
// @Tags Authentication
// @Accept x-www-form-urlencoded
// @Accept application/json
// @Param username formData string false "string" example(admin@admin.com)
// @Param password formData string false "string" example(admin)
// @Produce json
// @Success 200 {object} types.TokenResponse
// @Router /v1/users/login [POST]
func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
loginForm := &types.LoginForm{}
if r.Header.Get("Content-Type") == HeaderFormData {
err := r.ParseForm()
if err != nil {
server.Respond(w, http.StatusBadRequest, server.Wrap(err))
return
}
loginForm.Username = r.PostFormValue("username")
loginForm.Password = r.PostFormValue("password")
} else if r.Header.Get("Content-Type") == HeaderJSON {
err := server.Decode(r, loginForm)
if err != nil {
server.Respond(w, http.StatusBadRequest, server.Wrap(err))
return
}
} else {
server.Respond(w, http.StatusBadRequest, errors.New("invalid content type"))
return
}
if loginForm.Username == "" || loginForm.Password == "" {
server.RespondError(w, http.StatusBadRequest, errors.New("username and password are required"))
return
}
newToken, err := ctrl.svc.User.Login(r.Context(), loginForm.Username, loginForm.Password)
if err != nil {
server.RespondError(w, http.StatusUnauthorized, err)
return
}
err = server.Respond(w, http.StatusOK, types.TokenResponse{
BearerToken: "Bearer " + newToken.Raw,
ExpiresAt: newToken.ExpiresAt,
})
if err != nil {
ctrl.log.Error(err, logger.Props{
"user": loginForm.Username,
})
return
}
}
}
// HandleAuthLogout godoc
// @Summary User Logout
// @Tags Authentication
// @Success 204
// @Router /v1/users/logout [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleAuthLogout() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := services.UseTokenCtx(r.Context())
if token == "" {
server.RespondError(w, http.StatusUnauthorized, errors.New("no token within request context"))
return
}
err := ctrl.svc.User.Logout(r.Context(), token)
if err != nil {
server.RespondError(w, http.StatusInternalServerError, err)
return
}
err = server.Respond(w, http.StatusNoContent, nil)
}
}
// HandleAuthLogout godoc
// @Summary User Token Refresh
// @Description handleAuthRefresh returns a handler that will issue a new token from an existing token.
// @Description This does not validate that the user still exists within the database.
// @Tags Authentication
// @Success 200
// @Router /v1/users/refresh [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleAuthRefresh() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
requestToken := services.UseTokenCtx(r.Context())
if requestToken == "" {
server.RespondError(w, http.StatusUnauthorized, errors.New("no user token found"))
return
}
newToken, err := ctrl.svc.User.RenewToken(r.Context(), requestToken)
if err != nil {
server.RespondUnauthorized(w)
return
}
err = server.Respond(w, http.StatusOK, newToken)
if err != nil {
return
}
}
}

View file

@ -0,0 +1,80 @@
package v1
import (
"errors"
"net/http"
"github.com/hay-kot/git-web-template/backend/internal/services"
"github.com/hay-kot/git-web-template/backend/internal/types"
"github.com/hay-kot/git-web-template/backend/pkgs/logger"
"github.com/hay-kot/git-web-template/backend/pkgs/server"
)
// HandleUserSelf godoc
// @Summary Get the current user
// @Tags User
// @Produce json
// @Success 200 {object} server.Result{item=types.UserOut}
// @Router /v1/users/self [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleUserSelf() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := services.UseTokenCtx(r.Context())
usr, err := ctrl.svc.User.GetSelf(r.Context(), token)
if usr.IsNull() || err != nil {
ctrl.log.Error(errors.New("no user within request context"), nil)
server.RespondInternalServerError(w)
return
}
_ = server.Respond(w, http.StatusOK, server.Wrap(usr))
}
}
// HandleUserUpdate godoc
// @Summary Update the current user
// @Tags User
// @Produce json
// @Param payload body types.UserUpdate true "User Data"
// @Success 200 {object} server.Result{item=types.UserUpdate}
// @Router /v1/users/self [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleUserUpdate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
updateData := types.UserUpdate{}
if err := server.Decode(r, &updateData); err != nil {
ctrl.log.Error(err, logger.Props{
"scope": "user",
"details": "failed to decode user update data",
})
server.RespondError(w, http.StatusBadRequest, err)
return
}
actor := services.UseUserCtx(r.Context())
newData, err := ctrl.svc.User.UpdateSelf(r.Context(), actor.ID, updateData)
if err != nil {
ctrl.log.Error(err, logger.Props{
"scope": "user",
"details": "failed to update user",
})
server.RespondError(w, http.StatusInternalServerError, err)
return
}
_ = server.Respond(w, http.StatusOK, server.Wrap(newData))
}
}
// HandleUserUpdatePassword godoc
// @Summary Update the current user's password // TODO:
// @Tags User
// @Produce json
// @Success 204
// @Router /v1/users/self/password [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleUserUpdatePassword() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
}
}