forked from mirrors/homebox
end-to-end testing setup
This commit is contained in:
parent
b4eb7d8ddc
commit
ad4c8c9ab4
41 changed files with 544 additions and 313 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -33,3 +33,7 @@ node_modules
|
||||||
go.work
|
go.work
|
||||||
.task/
|
.task/
|
||||||
backend/.env
|
backend/.env
|
||||||
|
|
||||||
|
# Output Directory for Nuxt/Frontend during build step
|
||||||
|
backend/app/api/public/*
|
||||||
|
!backend/app/api/public/.gitkeep
|
36
Dockerfile
Normal file
36
Dockerfile
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# Build Nuxt
|
||||||
|
FROM node:17-alpine as frontend-builder
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||||
|
RUN pnpm install --frozen-lockfile --shamefully-hoist
|
||||||
|
COPY frontend .
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Build API
|
||||||
|
FROM golang:alpine AS builder
|
||||||
|
RUN apk update
|
||||||
|
RUN apk upgrade
|
||||||
|
RUN apk add --update git build-base gcc g++
|
||||||
|
WORKDIR /go/src/app
|
||||||
|
COPY ./backend .
|
||||||
|
COPY --from=frontend-builder app/.output go/src/app/app/api/public
|
||||||
|
RUN go get -d -v ./...
|
||||||
|
RUN CGO_ENABLED=1 GOOS=linux go build -o /go/bin/api -v ./app/api/*.go
|
||||||
|
|
||||||
|
|
||||||
|
# Production Stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
COPY ./backend/config.template.yml /app/config.yml
|
||||||
|
COPY --from=builder /go/bin/api /app
|
||||||
|
|
||||||
|
RUN chmod +x /app/api
|
||||||
|
|
||||||
|
LABEL Name=homebox Version=0.0.1
|
||||||
|
EXPOSE 7745
|
||||||
|
WORKDIR /app
|
||||||
|
CMD [ "./api" ]
|
|
@ -31,11 +31,11 @@ tasks:
|
||||||
- cd backend && go test -race -coverprofile=coverage.out -covermode=atomic ./app/... ./internal/... ./pkgs/... -v -cover
|
- cd backend && go test -race -coverprofile=coverage.out -covermode=atomic ./app/... ./internal/... ./pkgs/... -v -cover
|
||||||
silent: true
|
silent: true
|
||||||
|
|
||||||
client:test:
|
test:integration:
|
||||||
cmds:
|
cmds:
|
||||||
- cd backend && go run ./app/api/ &
|
- cd backend && go run ./app/api/ &
|
||||||
- sleep 5
|
- sleep 5
|
||||||
- cd client && npm run test:ci
|
- cd frontend && pnpm run test:ci
|
||||||
silent: true
|
silent: true
|
||||||
|
|
||||||
docker:build:
|
docker:build:
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
# Build API
|
|
||||||
FROM golang:alpine AS builder
|
|
||||||
RUN apk add --no-cache git build-base
|
|
||||||
WORKDIR /go/src/app
|
|
||||||
COPY . .
|
|
||||||
RUN go get -d -v ./...
|
|
||||||
RUN go build -o /go/bin/api -v ./app/api/*.go
|
|
||||||
|
|
||||||
|
|
||||||
# Production Stage
|
|
||||||
FROM alpine:latest
|
|
||||||
|
|
||||||
RUN apk --no-cache add ca-certificates
|
|
||||||
COPY ./config.template.yml /app/config.yml
|
|
||||||
COPY --from=builder /go/bin/api /app
|
|
||||||
|
|
||||||
RUN chmod +x /app/api
|
|
||||||
RUN chmod +x /bin/manage
|
|
||||||
|
|
||||||
LABEL Name=gowebtemplate Version=0.0.1
|
|
||||||
EXPOSE 7745
|
|
||||||
WORKDIR /app
|
|
||||||
CMD [ "./api" ]
|
|
|
@ -36,7 +36,7 @@ func NewApp(conf *config.Config) *app {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *app) StartReoccurringTasks(t time.Duration, fn func()) {
|
func (a *app) StartBgTask(t time.Duration, fn func()) {
|
||||||
for {
|
for {
|
||||||
a.server.Background(fn)
|
a.server.Background(fn)
|
||||||
time.Sleep(t)
|
time.Sleep(t)
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
package base
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/hay-kot/content/backend/internal/types"
|
|
||||||
"github.com/hay-kot/content/backend/pkgs/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ReadyFunc func() bool
|
|
||||||
|
|
||||||
type BaseController struct {
|
|
||||||
svr *server.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBaseController(svr *server.Server) *BaseController {
|
|
||||||
h := &BaseController{
|
|
||||||
svr: svr,
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleBase godoc
|
|
||||||
// @Summary Retrieves the basic information about the API
|
|
||||||
// @Tags Base
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} server.Result{item=types.ApiSummary}
|
|
||||||
// @Router /status [GET]
|
|
||||||
func (ctrl *BaseController) HandleBase(ready ReadyFunc, versions ...string) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
data := types.ApiSummary{
|
|
||||||
Healthy: ready(),
|
|
||||||
Versions: versions,
|
|
||||||
Title: "Go API Template",
|
|
||||||
Message: "Welcome to the Go API Template Application!",
|
|
||||||
}
|
|
||||||
|
|
||||||
server.Respond(w, http.StatusOK, server.Wrap(data))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package base
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetTestHandler(t *testing.T) *BaseController {
|
|
||||||
return NewBaseController(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandlersv1_HandleBase(t *testing.T) {
|
|
||||||
// Setup
|
|
||||||
hdlrFunc := GetTestHandler(t).HandleBase(func() bool { return true }, "v1")
|
|
||||||
|
|
||||||
// Call Handler Func
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
hdlrFunc(rr, nil)
|
|
||||||
|
|
||||||
// Validate Status Code
|
|
||||||
if rr.Code != http.StatusOK {
|
|
||||||
t.Errorf("Expected status code to be %d, got %d", http.StatusOK, rr.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate Json Payload
|
|
||||||
expected := `{"item":{"health":true,"versions":["v1"],"title":"Go API Template","message":"Welcome to the Go API Template Application!"}}`
|
|
||||||
|
|
||||||
if rr.Body.String() != expected {
|
|
||||||
t.Errorf("Expected json to be %s, got %s", expected, rr.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -21,37 +21,6 @@ const docTemplate = `{
|
||||||
"host": "{{.Host}}",
|
"host": "{{.Host}}",
|
||||||
"basePath": "{{.BasePath}}",
|
"basePath": "{{.BasePath}}",
|
||||||
"paths": {
|
"paths": {
|
||||||
"/status": {
|
|
||||||
"get": {
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Base"
|
|
||||||
],
|
|
||||||
"summary": "Retrieves the basic information about the API",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/server.Result"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"item": {
|
|
||||||
"$ref": "#/definitions/types.ApiSummary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/v1/items": {
|
"/v1/items": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -544,6 +513,25 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/status": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Base"
|
||||||
|
],
|
||||||
|
"summary": "Retrieves the basic information about the API",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/types.ApiSummary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v1/users/login": {
|
"/v1/users/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
|
@ -726,6 +714,25 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"User"
|
||||||
|
],
|
||||||
|
"summary": "Deletes the user account",
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/users/self/password": {
|
"/v1/users/self/password": {
|
||||||
|
|
|
@ -13,37 +13,6 @@
|
||||||
},
|
},
|
||||||
"basePath": "/api",
|
"basePath": "/api",
|
||||||
"paths": {
|
"paths": {
|
||||||
"/status": {
|
|
||||||
"get": {
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Base"
|
|
||||||
],
|
|
||||||
"summary": "Retrieves the basic information about the API",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/server.Result"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"item": {
|
|
||||||
"$ref": "#/definitions/types.ApiSummary"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/v1/items": {
|
"/v1/items": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -536,6 +505,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/status": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Base"
|
||||||
|
],
|
||||||
|
"summary": "Retrieves the basic information about the API",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/types.ApiSummary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v1/users/login": {
|
"/v1/users/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
|
@ -718,6 +706,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"User"
|
||||||
|
],
|
||||||
|
"summary": "Deletes the user account",
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/users/self/password": {
|
"/v1/users/self/password": {
|
||||||
|
|
|
@ -580,23 +580,6 @@ info:
|
||||||
title: Go API Templates
|
title: Go API Templates
|
||||||
version: "1.0"
|
version: "1.0"
|
||||||
paths:
|
paths:
|
||||||
/status:
|
|
||||||
get:
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
allOf:
|
|
||||||
- $ref: '#/definitions/server.Result'
|
|
||||||
- properties:
|
|
||||||
item:
|
|
||||||
$ref: '#/definitions/types.ApiSummary'
|
|
||||||
type: object
|
|
||||||
summary: Retrieves the basic information about the API
|
|
||||||
tags:
|
|
||||||
- Base
|
|
||||||
/v1/items:
|
/v1/items:
|
||||||
get:
|
get:
|
||||||
produces:
|
produces:
|
||||||
|
@ -888,6 +871,18 @@ paths:
|
||||||
summary: updates a location
|
summary: updates a location
|
||||||
tags:
|
tags:
|
||||||
- Locations
|
- Locations
|
||||||
|
/v1/status:
|
||||||
|
get:
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/types.ApiSummary'
|
||||||
|
summary: Retrieves the basic information about the API
|
||||||
|
tags:
|
||||||
|
- Base
|
||||||
/v1/users/login:
|
/v1/users/login:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
@ -955,6 +950,17 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- User
|
- User
|
||||||
/v1/users/self:
|
/v1/users/self:
|
||||||
|
delete:
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: ""
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: Deletes the user account
|
||||||
|
tags:
|
||||||
|
- User
|
||||||
get:
|
get:
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
|
|
|
@ -30,6 +30,7 @@ func main() {
|
||||||
// Logger Init
|
// Logger Init
|
||||||
// zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
// zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||||
|
log.Level(zerolog.DebugLevel)
|
||||||
|
|
||||||
cfgFile := "config.yml"
|
cfgFile := "config.yml"
|
||||||
|
|
||||||
|
@ -89,7 +90,7 @@ func run(cfg *config.Config) error {
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Start Reoccurring Tasks
|
// Start Reoccurring Tasks
|
||||||
|
|
||||||
go app.StartReoccurringTasks(time.Duration(24)*time.Hour, func() {
|
go app.StartBgTask(time.Duration(24)*time.Hour, func() {
|
||||||
_, err := app.repos.AuthTokens.PurgeExpiredTokens(context.Background())
|
_, err := app.repos.AuthTokens.PurgeExpiredTokens(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
|
|
0
backend/app/api/public/.gitkeep
Normal file
0
backend/app/api/public/.gitkeep
Normal file
|
@ -1,17 +1,27 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"embed"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/hay-kot/content/backend/app/api/base"
|
|
||||||
_ "github.com/hay-kot/content/backend/app/api/docs"
|
_ "github.com/hay-kot/content/backend/app/api/docs"
|
||||||
v1 "github.com/hay-kot/content/backend/app/api/v1"
|
v1 "github.com/hay-kot/content/backend/app/api/v1"
|
||||||
"github.com/hay-kot/content/backend/internal/repo"
|
"github.com/hay-kot/content/backend/internal/repo"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware
|
httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed all:public/*
|
||||||
|
var public embed.FS
|
||||||
|
|
||||||
const prefix = "/api"
|
const prefix = "/api"
|
||||||
|
|
||||||
// registerRoutes registers all the routes for the API
|
// registerRoutes registers all the routes for the API
|
||||||
|
@ -22,54 +32,54 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Base Routes
|
// Base Routes
|
||||||
|
|
||||||
|
DumpEmbedContents()
|
||||||
|
|
||||||
r.Get("/swagger/*", httpSwagger.Handler(
|
r.Get("/swagger/*", httpSwagger.Handler(
|
||||||
httpSwagger.URL(fmt.Sprintf("%s://%s/swagger/doc.json", a.conf.Swagger.Scheme, a.conf.Swagger.Host)),
|
httpSwagger.URL(fmt.Sprintf("%s://%s/swagger/doc.json", a.conf.Swagger.Scheme, a.conf.Swagger.Host)),
|
||||||
))
|
))
|
||||||
|
|
||||||
// Server Favicon
|
|
||||||
r.Get("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.ServeFile(w, r, "static/favicon.ico")
|
|
||||||
})
|
|
||||||
|
|
||||||
baseHandler := base.NewBaseController(a.server)
|
|
||||||
r.Get(prefix+"/status", baseHandler.HandleBase(func() bool { return true }, "v1"))
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// API Version 1
|
// API Version 1
|
||||||
|
|
||||||
v1Base := v1.BaseUrlFunc(prefix)
|
v1Base := v1.BaseUrlFunc(prefix)
|
||||||
|
v1Ctrl := v1.NewControllerV1(a.services)
|
||||||
{
|
{
|
||||||
v1Handlers := v1.NewControllerV1(a.services)
|
r.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, "v1"))
|
||||||
r.Post(v1Base("/users/register"), v1Handlers.HandleUserRegistration())
|
|
||||||
r.Post(v1Base("/users/login"), v1Handlers.HandleAuthLogin())
|
r.Post(v1Base("/users/register"), v1Ctrl.HandleUserRegistration())
|
||||||
|
r.Post(v1Base("/users/login"), v1Ctrl.HandleAuthLogin())
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(a.mwAuthToken)
|
r.Use(a.mwAuthToken)
|
||||||
r.Get(v1Base("/users/self"), v1Handlers.HandleUserSelf())
|
r.Get(v1Base("/users/self"), v1Ctrl.HandleUserSelf())
|
||||||
r.Put(v1Base("/users/self"), v1Handlers.HandleUserUpdate())
|
r.Put(v1Base("/users/self"), v1Ctrl.HandleUserSelfUpdate())
|
||||||
r.Put(v1Base("/users/self/password"), v1Handlers.HandleUserUpdatePassword())
|
r.Delete(v1Base("/users/self"), v1Ctrl.HandleUserSelfDelete())
|
||||||
r.Post(v1Base("/users/logout"), v1Handlers.HandleAuthLogout())
|
r.Put(v1Base("/users/self/password"), v1Ctrl.HandleUserUpdatePassword())
|
||||||
r.Get(v1Base("/users/refresh"), v1Handlers.HandleAuthRefresh())
|
r.Post(v1Base("/users/logout"), v1Ctrl.HandleAuthLogout())
|
||||||
|
r.Get(v1Base("/users/refresh"), v1Ctrl.HandleAuthRefresh())
|
||||||
|
|
||||||
r.Get(v1Base("/locations"), v1Handlers.HandleLocationGetAll())
|
r.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll())
|
||||||
r.Post(v1Base("/locations"), v1Handlers.HandleLocationCreate())
|
r.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate())
|
||||||
r.Get(v1Base("/locations/{id}"), v1Handlers.HandleLocationGet())
|
r.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet())
|
||||||
r.Put(v1Base("/locations/{id}"), v1Handlers.HandleLocationUpdate())
|
r.Put(v1Base("/locations/{id}"), v1Ctrl.HandleLocationUpdate())
|
||||||
r.Delete(v1Base("/locations/{id}"), v1Handlers.HandleLocationDelete())
|
r.Delete(v1Base("/locations/{id}"), v1Ctrl.HandleLocationDelete())
|
||||||
|
|
||||||
r.Get(v1Base("/labels"), v1Handlers.HandleLabelsGetAll())
|
r.Get(v1Base("/labels"), v1Ctrl.HandleLabelsGetAll())
|
||||||
r.Post(v1Base("/labels"), v1Handlers.HandleLabelsCreate())
|
r.Post(v1Base("/labels"), v1Ctrl.HandleLabelsCreate())
|
||||||
r.Get(v1Base("/labels/{id}"), v1Handlers.HandleLabelGet())
|
r.Get(v1Base("/labels/{id}"), v1Ctrl.HandleLabelGet())
|
||||||
r.Put(v1Base("/labels/{id}"), v1Handlers.HandleLabelUpdate())
|
r.Put(v1Base("/labels/{id}"), v1Ctrl.HandleLabelUpdate())
|
||||||
r.Delete(v1Base("/labels/{id}"), v1Handlers.HandleLabelDelete())
|
r.Delete(v1Base("/labels/{id}"), v1Ctrl.HandleLabelDelete())
|
||||||
|
|
||||||
r.Get(v1Base("/items"), v1Handlers.HandleItemsGetAll())
|
r.Get(v1Base("/items"), v1Ctrl.HandleItemsGetAll())
|
||||||
r.Post(v1Base("/items"), v1Handlers.HandleItemsCreate())
|
r.Post(v1Base("/items"), v1Ctrl.HandleItemsCreate())
|
||||||
r.Get(v1Base("/items/{id}"), v1Handlers.HandleItemGet())
|
r.Get(v1Base("/items/{id}"), v1Ctrl.HandleItemGet())
|
||||||
r.Put(v1Base("/items/{id}"), v1Handlers.HandleItemUpdate())
|
r.Put(v1Base("/items/{id}"), v1Ctrl.HandleItemUpdate())
|
||||||
r.Delete(v1Base("/items/{id}"), v1Handlers.HandleItemDelete())
|
r.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.NotFound(NotFoundHandler)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +88,7 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
|
||||||
func (a *app) LogRoutes(r *chi.Mux) {
|
func (a *app) LogRoutes(r *chi.Mux) {
|
||||||
desiredSpaces := 10
|
desiredSpaces := 10
|
||||||
|
|
||||||
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
walkFunc := func(method string, route string, handler http.Handler, middleware ...func(http.Handler) http.Handler) error {
|
||||||
text := "[" + method + "]"
|
text := "[" + method + "]"
|
||||||
|
|
||||||
for len(text) < desiredSpaces {
|
for len(text) < desiredSpaces {
|
||||||
|
@ -93,3 +103,56 @@ func (a *app) LogRoutes(r *chi.Mux) {
|
||||||
fmt.Printf("Logging err: %s\n", err.Error())
|
fmt.Printf("Logging err: %s\n", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrDir = errors.New("path is dir")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
mime.AddExtensionType(".js", "application/javascript")
|
||||||
|
mime.AddExtensionType(".mjs", "application/javascript")
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryRead(fs embed.FS, prefix, requestedPath string, w http.ResponseWriter) error {
|
||||||
|
f, err := fs.Open(path.Join(prefix, requestedPath))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
stat, _ := f.Stat()
|
||||||
|
if stat.IsDir() {
|
||||||
|
return ErrDir
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := mime.TypeByExtension(filepath.Ext(requestedPath))
|
||||||
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
_, err = io.Copy(w, f)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotFoundHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := tryRead(public, "public", r.URL.Path, w)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debug().
|
||||||
|
Str("path", r.URL.Path).
|
||||||
|
Msg("served from embed not found - serving index.html")
|
||||||
|
err = tryRead(public, "public", "index.html", w)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DumpEmbedContents() {
|
||||||
|
// recursively prints all contents in the embed.FS
|
||||||
|
err := fs.WalkDir(public, ".", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(path)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/hay-kot/content/backend/ent"
|
||||||
"github.com/hay-kot/content/backend/internal/repo"
|
"github.com/hay-kot/content/backend/internal/repo"
|
||||||
"github.com/hay-kot/content/backend/internal/types"
|
"github.com/hay-kot/content/backend/internal/types"
|
||||||
"github.com/hay-kot/content/backend/pkgs/hasher"
|
"github.com/hay-kot/content/backend/pkgs/hasher"
|
||||||
|
@ -60,25 +61,28 @@ func (a *app) SeedDatabase(repos *repo.AllRepos) {
|
||||||
log.Fatal().Err(err).Msg("failed to create default group")
|
log.Fatal().Err(err).Msg("failed to create default group")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, user := range a.conf.Seed.Users {
|
for _, seedUser := range a.conf.Seed.Users {
|
||||||
|
|
||||||
// Check if User Exists
|
// Check if User Exists
|
||||||
usr, _ := repos.Users.GetOneEmail(context.Background(), user.Email)
|
usr, err := repos.Users.GetOneEmail(context.Background(), seedUser.Email)
|
||||||
|
if err != nil && !ent.IsNotFound(err) {
|
||||||
|
log.Fatal().Err(err).Msg("failed to get user")
|
||||||
|
}
|
||||||
|
|
||||||
if usr.ID != uuid.Nil {
|
if usr != nil && usr.ID != uuid.Nil {
|
||||||
log.Info().Str("email", user.Email).Msg("user already exists, skipping")
|
log.Info().Str("email", seedUser.Email).Msg("user already exists, skipping")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
hashedPw, err := hasher.HashPassword(user.Password)
|
hashedPw, err := hasher.HashPassword(seedUser.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed to hash password")
|
log.Fatal().Err(err).Msg("failed to hash password")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = repos.Users.Create(context.Background(), types.UserCreate{
|
_, err = repos.Users.Create(context.Background(), types.UserCreate{
|
||||||
Name: user.Name,
|
Name: seedUser.Name,
|
||||||
Email: user.Email,
|
Email: seedUser.Email,
|
||||||
IsSuperuser: user.IsSuperuser,
|
IsSuperuser: seedUser.IsSuperuser,
|
||||||
Password: hashedPw,
|
Password: hashedPw,
|
||||||
GroupID: group.ID,
|
GroupID: group.ID,
|
||||||
})
|
})
|
||||||
|
@ -87,6 +91,6 @@ func (a *app) SeedDatabase(repos *repo.AllRepos) {
|
||||||
log.Fatal().Err(err).Msg("failed to create user")
|
log.Fatal().Err(err).Msg("failed to create user")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("email", user.Email).Msg("created user")
|
log.Info().Str("email", seedUser.Email).Msg("created user")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/hay-kot/content/backend/internal/services"
|
"github.com/hay-kot/content/backend/internal/services"
|
||||||
|
"github.com/hay-kot/content/backend/internal/types"
|
||||||
|
"github.com/hay-kot/content/backend/pkgs/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
type V1Controller struct {
|
type V1Controller struct {
|
||||||
|
@ -24,3 +28,22 @@ func NewControllerV1(svc *services.AllServices) *V1Controller {
|
||||||
|
|
||||||
return ctrl
|
return ctrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReadyFunc func() bool
|
||||||
|
|
||||||
|
// HandleBase godoc
|
||||||
|
// @Summary Retrieves the basic information about the API
|
||||||
|
// @Tags Base
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} types.ApiSummary
|
||||||
|
// @Router /v1/status [GET]
|
||||||
|
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, versions ...string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
server.Respond(w, http.StatusOK, types.ApiSummary{
|
||||||
|
Healthy: ready(),
|
||||||
|
Versions: versions,
|
||||||
|
Title: "Go API Template",
|
||||||
|
Message: "Welcome to the Go API Template Application!",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -16,3 +18,25 @@ func Test_NewHandlerV1(t *testing.T) {
|
||||||
assert.Equal(t, "/testing/v1/v1/abc123", v1Base("/abc123"))
|
assert.Equal(t, "/testing/v1/v1/abc123", v1Base("/abc123"))
|
||||||
assert.Equal(t, "/testing/v1/v1/abc123", v1Base("/abc123"))
|
assert.Equal(t, "/testing/v1/v1/abc123", v1Base("/abc123"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandlersv1_HandleBase(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
hdlrFunc := mockHandler.HandleBase(func() bool { return true }, "v1")
|
||||||
|
|
||||||
|
// Call Handler Func
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
hdlrFunc(rr, nil)
|
||||||
|
|
||||||
|
// Validate Status Code
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status code to be %d, got %d", http.StatusOK, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Json Payload
|
||||||
|
expected := `{"health":true,"versions":["v1"],"title":"Go API Template","message":"Welcome to the Go API Template Application!"}`
|
||||||
|
|
||||||
|
if rr.Body.String() != expected {
|
||||||
|
t.Errorf("Expected json to be %s, got %s", expected, rr.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ func (ctrl *V1Controller) HandleUserSelf() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleUserUpdate godoc
|
// HandleUserSelfUpdate godoc
|
||||||
// @Summary Update the current user
|
// @Summary Update the current user
|
||||||
// @Tags User
|
// @Tags User
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
@ -66,7 +66,7 @@ func (ctrl *V1Controller) HandleUserSelf() http.HandlerFunc {
|
||||||
// @Success 200 {object} server.Result{item=types.UserUpdate}
|
// @Success 200 {object} server.Result{item=types.UserUpdate}
|
||||||
// @Router /v1/users/self [PUT]
|
// @Router /v1/users/self [PUT]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleUserUpdate() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleUserSelfUpdate() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
updateData := types.UserUpdate{}
|
updateData := types.UserUpdate{}
|
||||||
if err := server.Decode(r, &updateData); err != nil {
|
if err := server.Decode(r, &updateData); err != nil {
|
||||||
|
@ -99,3 +99,22 @@ func (ctrl *V1Controller) HandleUserUpdatePassword() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleUserSelfDelete godoc
|
||||||
|
// @Summary Deletes the user account
|
||||||
|
// @Tags User
|
||||||
|
// @Produce json
|
||||||
|
// @Success 204
|
||||||
|
// @Router /v1/users/self [DELETE]
|
||||||
|
// @Security Bearer
|
||||||
|
func (ctrl *V1Controller) HandleUserSelfDelete() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
actor := services.UseUserCtx(r.Context())
|
||||||
|
if err := ctrl.svc.User.DeleteSelf(r.Context(), actor.ID); err != nil {
|
||||||
|
server.RespondError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server.Respond(w, http.StatusNoContent, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ swagger:
|
||||||
host: localhost:7745
|
host: localhost:7745
|
||||||
scheme: http
|
scheme: http
|
||||||
web:
|
web:
|
||||||
port: 3915
|
port: 7745
|
||||||
host: 127.0.0.1
|
host:
|
||||||
database:
|
database:
|
||||||
driver: sqlite3
|
driver: sqlite3
|
||||||
sqlite-url: ./ent.db?_fk=1
|
sqlite-url: ./ent.db?_fk=1
|
||||||
|
|
|
@ -2,6 +2,7 @@ package schema
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"entgo.io/ent"
|
"entgo.io/ent"
|
||||||
|
"entgo.io/ent/dialect/entsql"
|
||||||
"entgo.io/ent/schema/edge"
|
"entgo.io/ent/schema/edge"
|
||||||
"entgo.io/ent/schema/field"
|
"entgo.io/ent/schema/field"
|
||||||
"github.com/hay-kot/content/backend/ent/schema/mixins"
|
"github.com/hay-kot/content/backend/ent/schema/mixins"
|
||||||
|
@ -33,9 +34,17 @@ func (Group) Fields() []ent.Field {
|
||||||
// Edges of the Home.
|
// Edges of the Home.
|
||||||
func (Group) Edges() []ent.Edge {
|
func (Group) Edges() []ent.Edge {
|
||||||
return []ent.Edge{
|
return []ent.Edge{
|
||||||
edge.To("users", User.Type),
|
edge.To("users", User.Type).Annotations(entsql.Annotation{
|
||||||
edge.To("locations", Location.Type),
|
OnDelete: entsql.Cascade,
|
||||||
edge.To("items", Item.Type),
|
}),
|
||||||
edge.To("labels", Label.Type),
|
edge.To("locations", Location.Type).Annotations(entsql.Annotation{
|
||||||
|
OnDelete: entsql.Cascade,
|
||||||
|
}),
|
||||||
|
edge.To("items", Item.Type).Annotations(entsql.Annotation{
|
||||||
|
OnDelete: entsql.Cascade,
|
||||||
|
}),
|
||||||
|
edge.To("labels", Label.Type).Annotations(entsql.Annotation{
|
||||||
|
OnDelete: entsql.Cascade,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package schema
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"entgo.io/ent"
|
"entgo.io/ent"
|
||||||
|
"entgo.io/ent/dialect/entsql"
|
||||||
"entgo.io/ent/schema/edge"
|
"entgo.io/ent/schema/edge"
|
||||||
"entgo.io/ent/schema/field"
|
"entgo.io/ent/schema/field"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -73,7 +74,9 @@ func (Item) Edges() []ent.Edge {
|
||||||
edge.From("location", Location.Type).
|
edge.From("location", Location.Type).
|
||||||
Ref("items").
|
Ref("items").
|
||||||
Unique(),
|
Unique(),
|
||||||
edge.To("fields", ItemField.Type),
|
edge.To("fields", ItemField.Type).Annotations(entsql.Annotation{
|
||||||
|
OnDelete: entsql.Cascade,
|
||||||
|
}),
|
||||||
edge.From("label", Label.Type).
|
edge.From("label", Label.Type).
|
||||||
Ref("items"),
|
Ref("items"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package schema
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"entgo.io/ent"
|
"entgo.io/ent"
|
||||||
|
"entgo.io/ent/dialect/entsql"
|
||||||
"entgo.io/ent/schema/edge"
|
"entgo.io/ent/schema/edge"
|
||||||
"entgo.io/ent/schema/field"
|
"entgo.io/ent/schema/field"
|
||||||
"github.com/hay-kot/content/backend/ent/schema/mixins"
|
"github.com/hay-kot/content/backend/ent/schema/mixins"
|
||||||
|
@ -44,6 +45,8 @@ func (User) Edges() []ent.Edge {
|
||||||
Ref("users").
|
Ref("users").
|
||||||
Required().
|
Required().
|
||||||
Unique(),
|
Unique(),
|
||||||
edge.To("auth_tokens", AuthTokens.Type),
|
edge.To("auth_tokens", AuthTokens.Type).Annotations(entsql.Annotation{
|
||||||
|
OnDelete: entsql.Cascade,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ type SwaggerConf struct {
|
||||||
|
|
||||||
type WebConfig struct {
|
type WebConfig struct {
|
||||||
Port string `yaml:"port" conf:"default:7745"`
|
Port string `yaml:"port" conf:"default:7745"`
|
||||||
Host string `yaml:"host" conf:"default:127.0.0.1"`
|
Host string `yaml:"host"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig parses the CLI/Config file and returns a Config struct. If the file argument is an empty string, the
|
// NewConfig parses the CLI/Config file and returns a Config struct. If the file argument is an empty string, the
|
||||||
|
|
|
@ -14,23 +14,17 @@ type EntUserRepository struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EntUserRepository) GetOneId(ctx context.Context, id uuid.UUID) (*ent.User, error) {
|
func (e *EntUserRepository) GetOneId(ctx context.Context, id uuid.UUID) (*ent.User, error) {
|
||||||
usr, err := e.db.User.Query().Where(user.ID(id)).Only(ctx)
|
return e.db.User.Query().
|
||||||
|
Where(user.ID(id)).
|
||||||
if err != nil {
|
WithGroup().
|
||||||
return usr, err
|
Only(ctx)
|
||||||
}
|
|
||||||
|
|
||||||
return usr, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EntUserRepository) GetOneEmail(ctx context.Context, email string) (*ent.User, error) {
|
func (e *EntUserRepository) GetOneEmail(ctx context.Context, email string) (*ent.User, error) {
|
||||||
usr, err := e.db.User.Query().Where(user.Email(email)).Only(ctx)
|
return e.db.User.Query().
|
||||||
|
Where(user.Email(email)).
|
||||||
if err != nil {
|
WithGroup().
|
||||||
return usr, err
|
Only(ctx)
|
||||||
}
|
|
||||||
|
|
||||||
return usr, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EntUserRepository) GetAll(ctx context.Context) ([]*ent.User, error) {
|
func (e *EntUserRepository) GetAll(ctx context.Context) ([]*ent.User, error) {
|
||||||
|
@ -59,7 +53,11 @@ func (e *EntUserRepository) Create(ctx context.Context, usr types.UserCreate) (*
|
||||||
SetGroupID(usr.GroupID).
|
SetGroupID(usr.GroupID).
|
||||||
Save(ctx)
|
Save(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
return entUser, err
|
return entUser, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.GetOneId(ctx, entUser.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EntUserRepository) Update(ctx context.Context, ID uuid.UUID, data types.UserUpdate) error {
|
func (e *EntUserRepository) Update(ctx context.Context, ID uuid.UUID, data types.UserUpdate) error {
|
||||||
|
|
20
backend/internal/services/mappers/user.go
Normal file
20
backend/internal/services/mappers/user.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hay-kot/content/backend/ent"
|
||||||
|
"github.com/hay-kot/content/backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ToOutUser(user *ent.User, err error) (*types.UserOut, error) {
|
||||||
|
if err != nil {
|
||||||
|
return &types.UserOut{}, err
|
||||||
|
}
|
||||||
|
return &types.UserOut{
|
||||||
|
ID: user.ID,
|
||||||
|
Name: user.Name,
|
||||||
|
Email: user.Email,
|
||||||
|
IsSuperuser: user.IsSuperuser,
|
||||||
|
GroupName: user.Edges.Group.Name,
|
||||||
|
GroupID: user.Edges.Group.ID,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/hay-kot/content/backend/ent"
|
|
||||||
"github.com/hay-kot/content/backend/internal/repo"
|
"github.com/hay-kot/content/backend/internal/repo"
|
||||||
|
"github.com/hay-kot/content/backend/internal/services/mappers"
|
||||||
"github.com/hay-kot/content/backend/internal/types"
|
"github.com/hay-kot/content/backend/internal/types"
|
||||||
"github.com/hay-kot/content/backend/pkgs/hasher"
|
"github.com/hay-kot/content/backend/pkgs/hasher"
|
||||||
)
|
)
|
||||||
|
@ -23,24 +23,6 @@ type UserService struct {
|
||||||
repos *repo.AllRepos
|
repos *repo.AllRepos
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToOutUser(user *ent.User, err error) (*types.UserOut, error) {
|
|
||||||
if err != nil {
|
|
||||||
return &types.UserOut{}, err
|
|
||||||
}
|
|
||||||
return &types.UserOut{
|
|
||||||
ID: user.ID,
|
|
||||||
Name: user.Name,
|
|
||||||
Email: user.Email,
|
|
||||||
IsSuperuser: user.IsSuperuser,
|
|
||||||
GroupName: user.Edges.Group.Name,
|
|
||||||
GroupID: user.Edges.Group.ID,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (UserService) toOutUser(user *ent.User, err error) (*types.UserOut, error) {
|
|
||||||
return ToOutUser(user, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *UserService) RegisterUser(ctx context.Context, data types.UserRegistration) (*types.UserOut, error) {
|
func (svc *UserService) RegisterUser(ctx context.Context, data types.UserRegistration) (*types.UserOut, error) {
|
||||||
group, err := svc.repos.Groups.Create(ctx, data.GroupName)
|
group, err := svc.repos.Groups.Create(ctx, data.GroupName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -48,7 +30,6 @@ func (svc *UserService) RegisterUser(ctx context.Context, data types.UserRegistr
|
||||||
}
|
}
|
||||||
|
|
||||||
hashed, _ := hasher.HashPassword(data.User.Password)
|
hashed, _ := hasher.HashPassword(data.User.Password)
|
||||||
|
|
||||||
usrCreate := types.UserCreate{
|
usrCreate := types.UserCreate{
|
||||||
Name: data.User.Name,
|
Name: data.User.Name,
|
||||||
Email: data.User.Email,
|
Email: data.User.Email,
|
||||||
|
@ -57,23 +38,22 @@ func (svc *UserService) RegisterUser(ctx context.Context, data types.UserRegistr
|
||||||
GroupID: group.ID,
|
GroupID: group.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
return svc.toOutUser(svc.repos.Users.Create(ctx, usrCreate))
|
return mappers.ToOutUser(svc.repos.Users.Create(ctx, usrCreate))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSelf returns the user that is currently logged in based of the token provided within
|
// GetSelf returns the user that is currently logged in based of the token provided within
|
||||||
func (svc *UserService) GetSelf(ctx context.Context, requestToken string) (*types.UserOut, error) {
|
func (svc *UserService) GetSelf(ctx context.Context, requestToken string) (*types.UserOut, error) {
|
||||||
hash := hasher.HashToken(requestToken)
|
hash := hasher.HashToken(requestToken)
|
||||||
return svc.toOutUser(svc.repos.AuthTokens.GetUserFromToken(ctx, hash))
|
return mappers.ToOutUser(svc.repos.AuthTokens.GetUserFromToken(ctx, hash))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *UserService) UpdateSelf(ctx context.Context, ID uuid.UUID, data types.UserUpdate) (*types.UserOut, error) {
|
func (svc *UserService) UpdateSelf(ctx context.Context, ID uuid.UUID, data types.UserUpdate) (*types.UserOut, error) {
|
||||||
err := svc.repos.Users.Update(ctx, ID, data)
|
err := svc.repos.Users.Update(ctx, ID, data)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &types.UserOut{}, err
|
return &types.UserOut{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return svc.toOutUser(svc.repos.Users.GetOneId(ctx, ID))
|
return mappers.ToOutUser(svc.repos.Users.GetOneId(ctx, ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
@ -120,3 +100,10 @@ func (svc *UserService) RenewToken(ctx context.Context, token string) (types.Use
|
||||||
|
|
||||||
return newToken, nil
|
return newToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteSelf deletes the user that is currently logged based of the provided UUID
|
||||||
|
// There is _NO_ protection against deleting the wrong user, as such this should only
|
||||||
|
// be used when the identify of the user has been confirmed.
|
||||||
|
func (svc *UserService) DeleteSelf(ctx context.Context, ID uuid.UUID) error {
|
||||||
|
return svc.repos.Users.Delete(ctx, ID)
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
version: "3.4"
|
version: "3.4"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
gowebtemplate:
|
homebox:
|
||||||
image: gowebtemplate
|
image: homebox
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./Dockerfile
|
dockerfile: ./Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- 3001:7745
|
- 3100:7745
|
|
@ -59,10 +59,8 @@
|
||||||
const item = props.items[index];
|
const item = props.items[index];
|
||||||
|
|
||||||
if (selectedIndexes.value[index]) {
|
if (selectedIndexes.value[index]) {
|
||||||
console.log(value);
|
|
||||||
value.value = [...value.value, item];
|
value.value = [...value.value, item];
|
||||||
} else {
|
} else {
|
||||||
console.log(value);
|
|
||||||
value.value = value.value.filter(itm => itm !== item);
|
value.value = value.value.filter(itm => itm !== item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,8 +38,6 @@
|
||||||
|
|
||||||
const value = useVModel(props, 'modelValue', emit);
|
const value = useVModel(props, 'modelValue', emit);
|
||||||
const valueLen = computed(() => {
|
const valueLen = computed(() => {
|
||||||
console.log(value.value.length);
|
|
||||||
|
|
||||||
return value.value ? value.value.length : 0;
|
return value.value ? value.value.length : 0;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
57
frontend/lib/api/__test__/public.test.ts
Normal file
57
frontend/lib/api/__test__/public.test.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Requests } from '../../requests';
|
||||||
|
import { OverrideParts } from '../base/urls';
|
||||||
|
import { PublicApi } from '../public';
|
||||||
|
import * as config from '../../../test/config';
|
||||||
|
import { UserApi } from '../user';
|
||||||
|
|
||||||
|
function client() {
|
||||||
|
OverrideParts(config.BASE_URL, '/api/v1');
|
||||||
|
const requests = new Requests('');
|
||||||
|
return new PublicApi(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
function userClient(token: string) {
|
||||||
|
OverrideParts(config.BASE_URL, '/api/v1');
|
||||||
|
const requests = new Requests('', token);
|
||||||
|
return new UserApi(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('[GET] /api/v1/status', () => {
|
||||||
|
it('basic query parameter', async () => {
|
||||||
|
const api = client();
|
||||||
|
const { response, data } = await api.status();
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(data.health).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('first time user workflow (register, login)', () => {
|
||||||
|
const api = client();
|
||||||
|
const userData = {
|
||||||
|
groupName: 'test-group',
|
||||||
|
user: {
|
||||||
|
email: 'test-user@email.com',
|
||||||
|
name: 'test-user',
|
||||||
|
password: 'test-password',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('user should be able to register', async () => {
|
||||||
|
const { response } = await api.register(userData);
|
||||||
|
expect(response.status).toBe(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('user should be able to login', async () => {
|
||||||
|
const { response, data } = await api.login(userData.user.email, userData.user.password);
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(data.token).toBeTruthy();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
const userApi = userClient(data.token);
|
||||||
|
{
|
||||||
|
const { response } = await userApi.deleteAccount();
|
||||||
|
expect(response.status).toBe(204);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,20 +1,17 @@
|
||||||
export const prefix = '/api/v1';
|
const parts = {
|
||||||
|
host: 'http://localhost.com',
|
||||||
|
prefix: '/api/v1',
|
||||||
|
};
|
||||||
|
|
||||||
export type QueryValue =
|
export function OverrideParts(host: string, prefix: string) {
|
||||||
| string
|
parts.host = host;
|
||||||
| string[]
|
parts.prefix = prefix;
|
||||||
| number
|
}
|
||||||
| number[]
|
|
||||||
| boolean
|
|
||||||
| null
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
export function UrlBuilder(
|
export type QueryValue = string | string[] | number | number[] | boolean | null | undefined;
|
||||||
rest: string,
|
|
||||||
params: Record<string, QueryValue> = {}
|
export function UrlBuilder(rest: string, params: Record<string, QueryValue> = {}): string {
|
||||||
): string {
|
const url = new URL(parts.prefix + rest, parts.host);
|
||||||
// we use a stub base URL to leverage the URL class
|
|
||||||
const url = new URL(prefix + rest, 'http://localhost.com');
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(params)) {
|
for (const [key, value] of Object.entries(params)) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
|
|
|
@ -19,7 +19,18 @@ export type RegisterPayload = {
|
||||||
groupName: string;
|
groupName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type StatusResult = {
|
||||||
|
health: boolean;
|
||||||
|
versions: string[];
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class PublicApi extends BaseAPI {
|
export class PublicApi extends BaseAPI {
|
||||||
|
public status() {
|
||||||
|
return this.http.get<StatusResult>(UrlBuilder('/status'));
|
||||||
|
}
|
||||||
|
|
||||||
public login(username: string, password: string) {
|
public login(username: string, password: string) {
|
||||||
return this.http.post<LoginPayload, LoginResult>(UrlBuilder('/users/login'), {
|
return this.http.post<LoginPayload, LoginResult>(UrlBuilder('/users/login'), {
|
||||||
username,
|
username,
|
||||||
|
|
|
@ -36,4 +36,8 @@ export class UserApi extends BaseAPI {
|
||||||
public logout() {
|
public logout() {
|
||||||
return this.http.post<object, void>(UrlBuilder('/users/logout'), {});
|
return this.http.post<object, void>(UrlBuilder('/users/logout'), {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public deleteAccount() {
|
||||||
|
return this.http.delete<void>(UrlBuilder('/users/self'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,14 @@ import { defineNuxtConfig } from 'nuxt';
|
||||||
|
|
||||||
// https://v3.nuxtjs.org/api/configuration/nuxt.config
|
// https://v3.nuxtjs.org/api/configuration/nuxt.config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
|
target: 'static',
|
||||||
ssr: false,
|
ssr: false,
|
||||||
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt', '@vueuse/nuxt'],
|
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt', '@vueuse/nuxt'],
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Homebox',
|
title: 'Homebox',
|
||||||
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.svg' }],
|
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.svg' }],
|
||||||
},
|
},
|
||||||
|
outDir: '../backend/app/api/public',
|
||||||
vite: {
|
vite: {
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt generate",
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare",
|
"postinstall": "nuxt prepare",
|
||||||
"test": "vitest",
|
"test:ci": "TEST_SHUTDOWN_API_SERVER=true vitest --run --config ./test/vitest.config.ts",
|
||||||
"test:ci": "vitest --run"
|
"test:local": "TEST_SHUTDOWN_API_SERVER=false && vitest --run --config ./test/vitest.config.ts",
|
||||||
|
"test:watch": " TEST_SHUTDOWN_API_SERVER=false vitest --config ./test/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"isomorphic-fetch": "^3.0.0",
|
||||||
"nuxt": "3.0.0-rc.8",
|
"nuxt": "3.0.0-rc.8",
|
||||||
"vitest": "^0.22.1"
|
"vitest": "^0.22.1"
|
||||||
},
|
},
|
||||||
|
|
|
@ -92,7 +92,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success('Label updated');
|
toast.success('Label updated');
|
||||||
console.log(data);
|
|
||||||
label.value = data;
|
label.value = data;
|
||||||
updateModal.value = false;
|
updateModal.value = false;
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
|
|
|
@ -92,7 +92,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success('Location updated');
|
toast.success('Location updated');
|
||||||
console.log(data);
|
|
||||||
location.value = data;
|
location.value = data;
|
||||||
updateModal.value = false;
|
updateModal.value = false;
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
|
|
|
@ -10,6 +10,7 @@ specifiers:
|
||||||
'@vueuse/nuxt': ^9.1.1
|
'@vueuse/nuxt': ^9.1.1
|
||||||
autoprefixer: ^10.4.8
|
autoprefixer: ^10.4.8
|
||||||
daisyui: ^2.24.0
|
daisyui: ^2.24.0
|
||||||
|
isomorphic-fetch: ^3.0.0
|
||||||
nuxt: 3.0.0-rc.8
|
nuxt: 3.0.0-rc.8
|
||||||
pinia: ^2.0.21
|
pinia: ^2.0.21
|
||||||
postcss: ^8.4.16
|
postcss: ^8.4.16
|
||||||
|
@ -33,6 +34,7 @@ dependencies:
|
||||||
vue: 3.2.38
|
vue: 3.2.38
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
isomorphic-fetch: 3.0.0
|
||||||
nuxt: 3.0.0-rc.8
|
nuxt: 3.0.0-rc.8
|
||||||
vitest: 0.22.1
|
vitest: 0.22.1
|
||||||
|
|
||||||
|
@ -3180,6 +3182,15 @@ packages:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/isomorphic-fetch/3.0.0:
|
||||||
|
resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==}
|
||||||
|
dependencies:
|
||||||
|
node-fetch: 2.6.7
|
||||||
|
whatwg-fetch: 3.6.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
dev: true
|
||||||
|
|
||||||
/jest-worker/26.6.2:
|
/jest-worker/26.6.2:
|
||||||
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
|
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
|
||||||
engines: {node: '>= 10.13.0'}
|
engines: {node: '>= 10.13.0'}
|
||||||
|
@ -5720,6 +5731,10 @@ packages:
|
||||||
/webpack-virtual-modules/0.4.4:
|
/webpack-virtual-modules/0.4.4:
|
||||||
resolution: {integrity: sha512-h9atBP/bsZohWpHnr+2sic8Iecb60GxftXsWNLLLSqewgIsGzByd2gcIID4nXcG+3tNe4GQG3dLcff3kXupdRA==}
|
resolution: {integrity: sha512-h9atBP/bsZohWpHnr+2sic8Iecb60GxftXsWNLLLSqewgIsGzByd2gcIID4nXcG+3tNe4GQG3dLcff3kXupdRA==}
|
||||||
|
|
||||||
|
/whatwg-fetch/3.6.2:
|
||||||
|
resolution: {integrity: sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/whatwg-url/5.0.0:
|
/whatwg-url/5.0.0:
|
||||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
4
frontend/test/config.ts
Normal file
4
frontend/test/config.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export const PORT = "7745";
|
||||||
|
export const HOST = "http://127.0.0.1";
|
||||||
|
export const BASE_URL = HOST + ":" + PORT;
|
||||||
|
|
20
frontend/test/setup.ts
Normal file
20
frontend/test/setup.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import * as config from './config';
|
||||||
|
|
||||||
|
export const setup = () => {
|
||||||
|
console.log('Starting Client Tests');
|
||||||
|
console.log({
|
||||||
|
PORT: config.PORT,
|
||||||
|
HOST: config.HOST,
|
||||||
|
BASE_URL: config.BASE_URL,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const teardown = () => {
|
||||||
|
if (process.env.TEST_SHUTDOWN_API_SERVER) {
|
||||||
|
const pc = exec('pkill -SIGTERM api'); // Kill background API process
|
||||||
|
pc.stdout.on('data', data => {
|
||||||
|
console.log(`stdout: ${data}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
8
frontend/test/vitest.config.ts
Normal file
8
frontend/test/vitest.config.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/// <reference types="vitest" />
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globalSetup: "./test/setup.ts",
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in a new issue