forked from mirrors/homebox
Initial commit
This commit is contained in:
commit
29f583e936
135 changed files with 18463 additions and 0 deletions
46
backend/app/api/app.go
Normal file
46
backend/app/api/app.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/git-web-template/backend/ent"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/config"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/repo"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/services"
|
||||
"github.com/hay-kot/git-web-template/backend/pkgs/logger"
|
||||
"github.com/hay-kot/git-web-template/backend/pkgs/mailer"
|
||||
"github.com/hay-kot/git-web-template/backend/pkgs/server"
|
||||
)
|
||||
|
||||
type app struct {
|
||||
conf *config.Config
|
||||
logger *logger.Logger
|
||||
mailer mailer.Mailer
|
||||
db *ent.Client
|
||||
server *server.Server
|
||||
repos *repo.AllRepos
|
||||
services *services.AllServices
|
||||
}
|
||||
|
||||
func NewApp(conf *config.Config) *app {
|
||||
s := &app{
|
||||
conf: conf,
|
||||
}
|
||||
|
||||
s.mailer = mailer.Mailer{
|
||||
Host: s.conf.Mailer.Host,
|
||||
Port: s.conf.Mailer.Port,
|
||||
Username: s.conf.Mailer.Username,
|
||||
Password: s.conf.Mailer.Password,
|
||||
From: s.conf.Mailer.From,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (a *app) StartReoccurringTasks(t time.Duration, fn func()) {
|
||||
for {
|
||||
a.server.Background(fn)
|
||||
time.Sleep(t)
|
||||
}
|
||||
}
|
48
backend/app/api/base/base_ctrl.go
Normal file
48
backend/app/api/base/base_ctrl.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type ReadyFunc func() bool
|
||||
|
||||
type BaseController struct {
|
||||
log *logger.Logger
|
||||
svr *server.Server
|
||||
}
|
||||
|
||||
func NewBaseController(log *logger.Logger, svr *server.Server) *BaseController {
|
||||
h := &BaseController{
|
||||
log: log,
|
||||
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!",
|
||||
}
|
||||
|
||||
err := server.Respond(w, http.StatusOK, server.Wrap(data))
|
||||
|
||||
if err != nil {
|
||||
ctrl.log.Error(err, nil)
|
||||
server.RespondInternalServerError(w)
|
||||
}
|
||||
}
|
||||
}
|
35
backend/app/api/base/base_ctrl_test.go
Normal file
35
backend/app/api/base/base_ctrl_test.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/hay-kot/git-web-template/backend/internal/mocks"
|
||||
)
|
||||
|
||||
func GetTestHandler(t *testing.T) *BaseController {
|
||||
return NewBaseController(mocks.GetStructLogger(), 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())
|
||||
}
|
||||
|
||||
}
|
558
backend/app/api/docs/docs.go
Normal file
558
backend/app/api/docs/docs.go
Normal file
|
@ -0,0 +1,558 @@
|
|||
// Package docs GENERATED BY SWAG; DO NOT EDIT
|
||||
// This file was generated by swaggo/swag
|
||||
package docs
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
|
||||
const docTemplate = `{
|
||||
"schemes": {{ marshal .Schemes }},
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "{{escape .Description}}",
|
||||
"title": "{{.Title}}",
|
||||
"contact": {
|
||||
"name": "Don't"
|
||||
},
|
||||
"license": {
|
||||
"name": "MIT"
|
||||
},
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"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/admin/users": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin: Users"
|
||||
],
|
||||
"summary": "Gets all users from the database",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/types.UserOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin: Users"
|
||||
],
|
||||
"summary": "Create a new user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "User Data",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/types.UserCreate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {
|
||||
"$ref": "#/definitions/types.UserOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/admin/users/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin: Users"
|
||||
],
|
||||
"summary": "Get a user from the database",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {
|
||||
"$ref": "#/definitions/types.UserOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin: Users"
|
||||
],
|
||||
"summary": "Update a User",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "User Data",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/types.UserUpdate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {
|
||||
"$ref": "#/definitions/types.UserOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin: Users"
|
||||
],
|
||||
"summary": "Delete a User",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/login": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/x-www-form-urlencoded",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "User Login",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"example": "admin@admin.com",
|
||||
"description": "string",
|
||||
"name": "username",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "admin",
|
||||
"description": "string",
|
||||
"name": "password",
|
||||
"in": "formData"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/types.TokenResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/logout": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "User Logout",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/refresh": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "handleAuthRefresh returns a handler that will issue a new token from an existing token.\nThis does not validate that the user still exists within the database.",
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "User Token Refresh",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/self": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Get the current user",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {
|
||||
"$ref": "#/definitions/types.UserOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Update the current user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "User Data",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/types.UserUpdate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {
|
||||
"$ref": "#/definitions/types.UserUpdate"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/self/password": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Update the current user's password // TODO:",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"server.Result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"details": {},
|
||||
"error": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"item": {},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"types.ApiSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"health": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"versions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"types.TokenResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expiresAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"types.UserCreate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"isSuperuser": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"types.UserOut": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isSuperuser": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"types.UserUpdate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"Bearer": {
|
||||
"description": "\"Type 'Bearer TOKEN' to correctly set the API Key\"",
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "1.0",
|
||||
Host: "",
|
||||
BasePath: "/api",
|
||||
Schemes: []string{},
|
||||
Title: "Go API Templates",
|
||||
Description: "This is a simple Rest API Server Template that implements some basic User and Authentication patterns to help you get started and bootstrap your next project!.",
|
||||
InfoInstanceName: "swagger",
|
||||
SwaggerTemplate: docTemplate,
|
||||
}
|
||||
|
||||
func init() {
|
||||
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||
}
|
534
backend/app/api/docs/swagger.json
Normal file
534
backend/app/api/docs/swagger.json
Normal file
|
@ -0,0 +1,534 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "This is a simple Rest API Server Template that implements some basic User and Authentication patterns to help you get started and bootstrap your next project!.",
|
||||
"title": "Go API Templates",
|
||||
"contact": {
|
||||
"name": "Don't"
|
||||
},
|
||||
"license": {
|
||||
"name": "MIT"
|
||||
},
|
||||
"version": "1.0"
|
||||
},
|
||||
"basePath": "/api",
|
||||
"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/admin/users": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin: Users"
|
||||
],
|
||||
"summary": "Gets all users from the database",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/types.UserOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin: Users"
|
||||
],
|
||||
"summary": "Create a new user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "User Data",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/types.UserCreate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {
|
||||
"$ref": "#/definitions/types.UserOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/admin/users/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin: Users"
|
||||
],
|
||||
"summary": "Get a user from the database",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {
|
||||
"$ref": "#/definitions/types.UserOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin: Users"
|
||||
],
|
||||
"summary": "Update a User",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "User Data",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/types.UserUpdate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {
|
||||
"$ref": "#/definitions/types.UserOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin: Users"
|
||||
],
|
||||
"summary": "Delete a User",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/login": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/x-www-form-urlencoded",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "User Login",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"example": "admin@admin.com",
|
||||
"description": "string",
|
||||
"name": "username",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "admin",
|
||||
"description": "string",
|
||||
"name": "password",
|
||||
"in": "formData"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/types.TokenResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/logout": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "User Logout",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/refresh": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "handleAuthRefresh returns a handler that will issue a new token from an existing token.\nThis does not validate that the user still exists within the database.",
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "User Token Refresh",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/self": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Get the current user",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {
|
||||
"$ref": "#/definitions/types.UserOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Update the current user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "User Data",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/types.UserUpdate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/server.Result"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": {
|
||||
"$ref": "#/definitions/types.UserUpdate"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/self/password": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Update the current user's password // TODO:",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"server.Result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"details": {},
|
||||
"error": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"item": {},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"types.ApiSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"health": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"versions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"types.TokenResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expiresAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"types.UserCreate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"isSuperuser": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"types.UserOut": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isSuperuser": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"types.UserUpdate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"Bearer": {
|
||||
"description": "\"Type 'Bearer TOKEN' to correctly set the API Key\"",
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}
|
318
backend/app/api/docs/swagger.yaml
Normal file
318
backend/app/api/docs/swagger.yaml
Normal file
|
@ -0,0 +1,318 @@
|
|||
basePath: /api
|
||||
definitions:
|
||||
server.Result:
|
||||
properties:
|
||||
details: {}
|
||||
error:
|
||||
type: boolean
|
||||
item: {}
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
types.ApiSummary:
|
||||
properties:
|
||||
health:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
versions:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
types.TokenResponse:
|
||||
properties:
|
||||
expiresAt:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
type: object
|
||||
types.UserCreate:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
isSuperuser:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
type: object
|
||||
types.UserOut:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
isSuperuser:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
types.UserUpdate:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
info:
|
||||
contact:
|
||||
name: Don't
|
||||
description: This is a simple Rest API Server Template that implements some basic
|
||||
User and Authentication patterns to help you get started and bootstrap your next
|
||||
project!.
|
||||
license:
|
||||
name: MIT
|
||||
title: Go API Templates
|
||||
version: "1.0"
|
||||
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/admin/users:
|
||||
get:
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/server.Result'
|
||||
- properties:
|
||||
item:
|
||||
items:
|
||||
$ref: '#/definitions/types.UserOut'
|
||||
type: array
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Gets all users from the database
|
||||
tags:
|
||||
- 'Admin: Users'
|
||||
post:
|
||||
parameters:
|
||||
- description: User Data
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/types.UserCreate'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/server.Result'
|
||||
- properties:
|
||||
item:
|
||||
$ref: '#/definitions/types.UserOut'
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Create a new user
|
||||
tags:
|
||||
- 'Admin: Users'
|
||||
/v1/admin/users/{id}:
|
||||
delete:
|
||||
parameters:
|
||||
- description: User ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Delete a User
|
||||
tags:
|
||||
- 'Admin: Users'
|
||||
get:
|
||||
parameters:
|
||||
- description: User ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/server.Result'
|
||||
- properties:
|
||||
item:
|
||||
$ref: '#/definitions/types.UserOut'
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Get a user from the database
|
||||
tags:
|
||||
- 'Admin: Users'
|
||||
put:
|
||||
parameters:
|
||||
- description: User ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: User Data
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/types.UserUpdate'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/server.Result'
|
||||
- properties:
|
||||
item:
|
||||
$ref: '#/definitions/types.UserOut'
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Update a User
|
||||
tags:
|
||||
- 'Admin: Users'
|
||||
/v1/users/login:
|
||||
post:
|
||||
consumes:
|
||||
- application/x-www-form-urlencoded
|
||||
- application/json
|
||||
parameters:
|
||||
- description: string
|
||||
example: admin@admin.com
|
||||
in: formData
|
||||
name: username
|
||||
type: string
|
||||
- description: string
|
||||
example: admin
|
||||
in: formData
|
||||
name: password
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/types.TokenResponse'
|
||||
summary: User Login
|
||||
tags:
|
||||
- Authentication
|
||||
/v1/users/logout:
|
||||
post:
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: User Logout
|
||||
tags:
|
||||
- Authentication
|
||||
/v1/users/refresh:
|
||||
get:
|
||||
description: |-
|
||||
handleAuthRefresh returns a handler that will issue a new token from an existing token.
|
||||
This does not validate that the user still exists within the database.
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: User Token Refresh
|
||||
tags:
|
||||
- Authentication
|
||||
/v1/users/self:
|
||||
get:
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/server.Result'
|
||||
- properties:
|
||||
item:
|
||||
$ref: '#/definitions/types.UserOut'
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Get the current user
|
||||
tags:
|
||||
- User
|
||||
put:
|
||||
parameters:
|
||||
- description: User Data
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/types.UserUpdate'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/server.Result'
|
||||
- properties:
|
||||
item:
|
||||
$ref: '#/definitions/types.UserUpdate'
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Update the current user
|
||||
tags:
|
||||
- User
|
||||
/v1/users/self/password:
|
||||
put:
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: ""
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 'Update the current user''s password // TODO:'
|
||||
tags:
|
||||
- User
|
||||
securityDefinitions:
|
||||
Bearer:
|
||||
description: '"Type ''Bearer TOKEN'' to correctly set the API Key"'
|
||||
in: header
|
||||
name: Authorization
|
||||
type: apiKey
|
||||
swagger: "2.0"
|
116
backend/app/api/main.go
Normal file
116
backend/app/api/main.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/git-web-template/backend/app/api/docs"
|
||||
"github.com/hay-kot/git-web-template/backend/ent"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/config"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/repo"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/services"
|
||||
"github.com/hay-kot/git-web-template/backend/pkgs/logger"
|
||||
"github.com/hay-kot/git-web-template/backend/pkgs/server"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// @title Go API Templates
|
||||
// @version 1.0
|
||||
// @description This is a simple Rest API Server Template that implements some basic User and Authentication patterns to help you get started and bootstrap your next project!.
|
||||
// @contact.name Don't
|
||||
// @license.name MIT
|
||||
// @BasePath /api
|
||||
// @securityDefinitions.apikey Bearer
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description "Type 'Bearer TOKEN' to correctly set the API Key"
|
||||
func main() {
|
||||
cfgFile := "config.yml"
|
||||
|
||||
cfg, err := config.NewConfig(cfgFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
docs.SwaggerInfo.Host = cfg.Swagger.Host
|
||||
|
||||
if err := run(cfg); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(cfg *config.Config) error {
|
||||
app := NewApp(cfg)
|
||||
|
||||
// =========================================================================
|
||||
// Setup Logger
|
||||
|
||||
var wrt io.Writer
|
||||
wrt = os.Stdout
|
||||
if app.conf.Log.File != "" {
|
||||
f, err := os.OpenFile(app.conf.Log.File, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
log.Fatalf("error opening file: %v", err)
|
||||
}
|
||||
defer func(f *os.File) {
|
||||
_ = f.Close()
|
||||
}(f)
|
||||
wrt = io.MultiWriter(wrt, f)
|
||||
}
|
||||
|
||||
app.logger = logger.New(wrt, logger.LevelDebug)
|
||||
|
||||
// =========================================================================
|
||||
// Initialize Database & Repos
|
||||
|
||||
c, err := ent.Open(cfg.Database.GetDriver(), cfg.Database.GetUrl())
|
||||
if err != nil {
|
||||
app.logger.Fatal(err, logger.Props{
|
||||
"details": "failed to connect to database",
|
||||
"database": cfg.Database.GetDriver(),
|
||||
"url": cfg.Database.GetUrl(),
|
||||
})
|
||||
}
|
||||
defer func(c *ent.Client) {
|
||||
_ = c.Close()
|
||||
}(c)
|
||||
if err := c.Schema.Create(context.Background()); err != nil {
|
||||
app.logger.Fatal(err, logger.Props{
|
||||
"details": "failed to create schema",
|
||||
})
|
||||
}
|
||||
|
||||
app.db = c
|
||||
app.repos = repo.EntAllRepos(c)
|
||||
app.services = services.NewServices(app.repos)
|
||||
|
||||
// =========================================================================
|
||||
// Start Server
|
||||
|
||||
app.conf.Print()
|
||||
|
||||
app.server = server.NewServer(app.conf.Web.Host, app.conf.Web.Port)
|
||||
|
||||
routes := app.newRouter(app.repos)
|
||||
app.LogRoutes(routes)
|
||||
|
||||
app.EnsureAdministrator()
|
||||
app.SeedDatabase(app.repos)
|
||||
|
||||
app.logger.Info("Starting HTTP Server", logger.Props{
|
||||
"host": app.server.Host,
|
||||
"port": app.server.Port,
|
||||
})
|
||||
|
||||
// =========================================================================
|
||||
// Start Reoccurring Tasks
|
||||
|
||||
go app.StartReoccurringTasks(time.Duration(24)*time.Hour, func() {
|
||||
app.repos.AuthTokens.PurgeExpiredTokens(context.Background())
|
||||
})
|
||||
|
||||
return app.server.Start(routes)
|
||||
}
|
117
backend/app/api/middleware.go
Normal file
117
backend/app/api/middleware.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/config"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/services"
|
||||
"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"
|
||||
)
|
||||
|
||||
func (a *app) setGlobalMiddleware(r *chi.Mux) {
|
||||
// =========================================================================
|
||||
// Middleware
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(mwStripTrailingSlash)
|
||||
|
||||
// Use struct logger in production for requests, but use
|
||||
// pretty console logger in development.
|
||||
if a.conf.Mode == config.ModeDevelopment {
|
||||
r.Use(middleware.Logger)
|
||||
} else {
|
||||
r.Use(a.mwStructLogger)
|
||||
}
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
// Set a timeout value on the request context (ctx), that will signal
|
||||
// through ctx.Done() that the request has timed out and further
|
||||
// processing should be stopped.
|
||||
r.Use(middleware.Timeout(60 * time.Second))
|
||||
}
|
||||
|
||||
// mwAuthToken is a middleware that will check the database for a stateful token
|
||||
// and attach it to the request context with the user, or return a 401 if it doesn't exist.
|
||||
func (a *app) mwAuthToken(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestToken := r.Header.Get("Authorization")
|
||||
|
||||
if requestToken == "" {
|
||||
server.RespondUnauthorized(w)
|
||||
return
|
||||
}
|
||||
|
||||
requestToken = strings.TrimPrefix(requestToken, "Bearer ")
|
||||
|
||||
hash := hasher.HashToken(requestToken)
|
||||
|
||||
// Check the database for the token
|
||||
usr, err := a.repos.AuthTokens.GetUserFromToken(r.Context(), hash)
|
||||
|
||||
if err != nil {
|
||||
a.logger.Error(err, logger.Props{
|
||||
"token": requestToken,
|
||||
"hash": fmt.Sprintf("%x", hash),
|
||||
})
|
||||
server.RespondUnauthorized(w)
|
||||
return
|
||||
}
|
||||
|
||||
r = r.WithContext(services.SetUserCtx(r.Context(), &usr, requestToken))
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// mwAdminOnly is a middleware that extends the mwAuthToken middleware to only allow
|
||||
// requests from superusers.
|
||||
func (a *app) mwAdminOnly(next http.Handler) http.Handler {
|
||||
|
||||
mw := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
usr := services.UseUserCtx(r.Context())
|
||||
|
||||
if !usr.IsSuperuser {
|
||||
server.RespondUnauthorized(w)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
return a.mwAuthToken(mw)
|
||||
}
|
||||
|
||||
// mqStripTrailingSlash is a middleware that will strip trailing slashes from the request path.
|
||||
func mwStripTrailingSlash(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = strings.TrimSuffix(r.URL.Path, "/")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (a *app) mwStructLogger(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s://%s%s %s", scheme, r.Host, r.RequestURI, r.Proto)
|
||||
|
||||
a.logger.Info(fmt.Sprintf("[%s] %s", r.Method, url), logger.Props{
|
||||
"id": middleware.GetReqID(r.Context()),
|
||||
"method": r.Method,
|
||||
"url": url,
|
||||
"remote": r.RemoteAddr,
|
||||
})
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
82
backend/app/api/routes.go
Normal file
82
backend/app/api/routes.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/hay-kot/git-web-template/backend/app/api/base"
|
||||
_ "github.com/hay-kot/git-web-template/backend/app/api/docs"
|
||||
v1 "github.com/hay-kot/git-web-template/backend/app/api/v1"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/repo"
|
||||
httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware
|
||||
)
|
||||
|
||||
const prefix = "/api"
|
||||
|
||||
// registerRoutes registers all the routes for the API
|
||||
func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
a.setGlobalMiddleware(r)
|
||||
|
||||
// =========================================================================
|
||||
// Base Routes
|
||||
|
||||
r.Get("/swagger/*", httpSwagger.Handler(
|
||||
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.logger, a.server)
|
||||
r.Get(prefix+"/status", baseHandler.HandleBase(func() bool { return true }, "v1"))
|
||||
|
||||
// =========================================================================
|
||||
// API Version 1
|
||||
v1Base := v1.BaseUrlFunc(prefix)
|
||||
v1Handlers := v1.NewControllerV1(a.logger, a.services)
|
||||
r.Post(v1Base("/users/login"), v1Handlers.HandleAuthLogin())
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(a.mwAuthToken)
|
||||
r.Get(v1Base("/users/self"), v1Handlers.HandleUserSelf())
|
||||
r.Put(v1Base("/users/self"), v1Handlers.HandleUserUpdate())
|
||||
r.Put(v1Base("/users/self/password"), v1Handlers.HandleUserUpdatePassword())
|
||||
r.Post(v1Base("/users/logout"), v1Handlers.HandleAuthLogout())
|
||||
r.Get(v1Base("/users/refresh"), v1Handlers.HandleAuthRefresh())
|
||||
})
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(a.mwAdminOnly)
|
||||
r.Get(v1Base("/admin/users"), v1Handlers.HandleAdminUserGetAll())
|
||||
r.Post(v1Base("/admin/users"), v1Handlers.HandleAdminUserCreate())
|
||||
r.Get(v1Base("/admin/users/{id}"), v1Handlers.HandleAdminUserGet())
|
||||
r.Put(v1Base("/admin/users/{id}"), v1Handlers.HandleAdminUserUpdate())
|
||||
r.Delete(v1Base("/admin/users/{id}"), v1Handlers.HandleAdminUserDelete())
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// LogRoutes logs the routes of the server that are registered within Server.registerRoutes(). This is useful for debugging.
|
||||
// See https://github.com/go-chi/chi/issues/332 for details and inspiration.
|
||||
func (a *app) LogRoutes(r *chi.Mux) {
|
||||
desiredSpaces := 10
|
||||
|
||||
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
text := "[" + method + "]"
|
||||
|
||||
for len(text) < desiredSpaces {
|
||||
text = text + " "
|
||||
}
|
||||
|
||||
fmt.Printf("Registered Route: %s%s\n", text, route)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := chi.Walk(r, walkFunc); err != nil {
|
||||
fmt.Printf("Logging err: %s\n", err.Error())
|
||||
}
|
||||
}
|
98
backend/app/api/seed.go
Normal file
98
backend/app/api/seed.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/repo"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultName = "Admin"
|
||||
DefaultEmail = "admin@admin.com"
|
||||
DefaultPassword = "admin"
|
||||
)
|
||||
|
||||
// EnsureAdministrator ensures that there is at least one superuser in the database
|
||||
// if one isn't found a default is generate using the default credentials
|
||||
func (a *app) EnsureAdministrator() {
|
||||
superusers, err := a.repos.Users.GetSuperusers(context.Background())
|
||||
|
||||
if err != nil {
|
||||
a.logger.Error(err, nil)
|
||||
}
|
||||
|
||||
if len(superusers) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
pw, _ := hasher.HashPassword(DefaultPassword)
|
||||
|
||||
newSuperUser := types.UserCreate{
|
||||
Name: DefaultName,
|
||||
Email: DefaultEmail,
|
||||
IsSuperuser: true,
|
||||
Password: pw,
|
||||
}
|
||||
|
||||
a.logger.Info("creating default superuser", logger.Props{
|
||||
"name": newSuperUser.Name,
|
||||
"email": newSuperUser.Email,
|
||||
})
|
||||
|
||||
_, err = a.repos.Users.Create(context.Background(), newSuperUser)
|
||||
|
||||
if err != nil {
|
||||
a.logger.Fatal(err, nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (a *app) SeedDatabase(repos *repo.AllRepos) {
|
||||
if !a.conf.Seed.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
for _, user := range a.conf.Seed.Users {
|
||||
|
||||
// Check if User Exists
|
||||
usr, _ := repos.Users.GetOneEmail(context.Background(), user.Email)
|
||||
|
||||
if usr.ID != uuid.Nil {
|
||||
a.logger.Info("seed user already exists", logger.Props{
|
||||
"user": user.Name,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
hashedPw, err := hasher.HashPassword(user.Password)
|
||||
|
||||
if err != nil {
|
||||
a.logger.Error(err, logger.Props{
|
||||
"details": "failed to hash password",
|
||||
"user": user.Name,
|
||||
})
|
||||
}
|
||||
|
||||
_, err = repos.Users.Create(context.Background(), types.UserCreate{
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
IsSuperuser: user.IsSuperuser,
|
||||
Password: hashedPw,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
a.logger.Error(err, logger.Props{
|
||||
"details": "failed to create seed user",
|
||||
"name": user.Name,
|
||||
})
|
||||
}
|
||||
|
||||
a.logger.Info("creating seed user", logger.Props{
|
||||
"name": user.Name,
|
||||
})
|
||||
}
|
||||
}
|
29
backend/app/api/v1/controller.go
Normal file
29
backend/app/api/v1/controller.go
Normal 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
|
||||
}
|
20
backend/app/api/v1/controller_test.go
Normal file
20
backend/app/api/v1/controller_test.go
Normal 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"))
|
||||
}
|
51
backend/app/api/v1/main_test.go
Normal file
51
backend/app/api/v1/main_test.go
Normal 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()
|
||||
}
|
207
backend/app/api/v1/v1_ctrl_admin.go
Normal file
207
backend/app/api/v1/v1_ctrl_admin.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
109
backend/app/api/v1/v1_ctrl_admin_test.go
Normal file
109
backend/app/api/v1/v1_ctrl_admin_test.go
Normal 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()
|
||||
}
|
136
backend/app/api/v1/v1_ctrl_auth.go
Normal file
136
backend/app/api/v1/v1_ctrl_auth.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
80
backend/app/api/v1/v1_ctrl_user.go
Normal file
80
backend/app/api/v1/v1_ctrl_user.go
Normal 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) {
|
||||
}
|
||||
}
|
9
backend/app/cli/app.go
Normal file
9
backend/app/cli/app.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/hay-kot/git-web-template/backend/internal/repo"
|
||||
)
|
||||
|
||||
type app struct {
|
||||
repos *repo.AllRepos
|
||||
}
|
105
backend/app/cli/app_users.go
Normal file
105
backend/app/cli/app_users.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/git-web-template/backend/app/cli/reader"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/types"
|
||||
"github.com/hay-kot/git-web-template/backend/pkgs/hasher"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func (a *app) UserCreate(c *cli.Context) error {
|
||||
var defaultValidators = []reader.StringValidator{
|
||||
reader.StringRequired,
|
||||
reader.StringNoLeadingOrTrailingWhitespace,
|
||||
}
|
||||
// Get Flags
|
||||
name := reader.ReadString("Name: ",
|
||||
defaultValidators...,
|
||||
)
|
||||
password := reader.ReadString("Password: ",
|
||||
defaultValidators...,
|
||||
)
|
||||
|
||||
email := reader.ReadString("Email: ",
|
||||
reader.StringRequired,
|
||||
reader.StringNoLeadingOrTrailingWhitespace,
|
||||
reader.StringContainsAt,
|
||||
)
|
||||
isSuper := reader.ReadBool("Is Superuser?")
|
||||
|
||||
pwHash, err := hasher.HashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usr := types.UserCreate{
|
||||
Name: name,
|
||||
Email: email,
|
||||
Password: pwHash,
|
||||
IsSuperuser: isSuper,
|
||||
}
|
||||
|
||||
_, err = a.repos.Users.Create(context.Background(), usr)
|
||||
|
||||
if err == nil {
|
||||
fmt.Println("Super user created")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *app) UserDelete(c *cli.Context) error {
|
||||
// Get Flags
|
||||
id := c.String("id")
|
||||
uid := uuid.MustParse(id)
|
||||
|
||||
fmt.Printf("Deleting user with id: %s\n", id)
|
||||
|
||||
// Confirm Action
|
||||
fmt.Printf("Are you sure you want to delete this user? (y/n) ")
|
||||
var answer string
|
||||
_, err := fmt.Scanln(&answer)
|
||||
if answer != "y" || err != nil {
|
||||
fmt.Println("Aborting")
|
||||
return nil
|
||||
}
|
||||
|
||||
err = a.repos.Users.Delete(context.Background(), uid)
|
||||
|
||||
if err == nil {
|
||||
fmt.Printf("%v User(s) deleted (id=%v)\n", 1, id)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *app) UserList(c *cli.Context) error {
|
||||
fmt.Println("Superuser List")
|
||||
|
||||
users, err := a.repos.Users.GetAll(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tabWriter := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
defer func(tabWriter *tabwriter.Writer) {
|
||||
_ = tabWriter.Flush()
|
||||
}(tabWriter)
|
||||
|
||||
_, err = fmt.Fprintln(tabWriter, "Id\tName\tEmail\tIsSuper")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
_, _ = fmt.Fprintf(tabWriter, "%v\t%s\t%s\t%v\n", u.ID, u.Name, u.Email, u.IsSuperuser)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
82
backend/app/cli/main.go
Normal file
82
backend/app/cli/main.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/hay-kot/git-web-template/backend/ent"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/config"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/repo"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.NewConfig("config.yml")
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := run(cfg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(cfg *config.Config) error {
|
||||
// =========================================================================
|
||||
// Initialize Database
|
||||
c, err := ent.Open(cfg.Database.GetDriver(), cfg.Database.GetUrl())
|
||||
if err != nil {
|
||||
log.Fatalf("failed opening connection to sqlite: %v", err)
|
||||
}
|
||||
defer func(c *ent.Client) {
|
||||
_ = c.Close()
|
||||
}(c)
|
||||
if err := c.Schema.Create(context.Background()); err != nil {
|
||||
log.Fatalf("failed creating schema resources: %v", err)
|
||||
}
|
||||
|
||||
// Create App
|
||||
a := &app{
|
||||
repos: repo.EntAllRepos(c),
|
||||
}
|
||||
|
||||
app := &cli.App{
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "users",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "options to manage users",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "list users in database",
|
||||
Action: a.UserList,
|
||||
},
|
||||
{
|
||||
Name: "add",
|
||||
Usage: "add a new user",
|
||||
Action: a.UserCreate,
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Usage: "delete user in database",
|
||||
Action: a.UserDelete,
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: "id",
|
||||
Usage: "name of the user to add",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return app.Run(os.Args)
|
||||
}
|
65
backend/app/cli/reader/reader.go
Normal file
65
backend/app/cli/reader/reader.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package reader
|
||||
|
||||
import "fmt"
|
||||
|
||||
type StringValidator func(s string) bool
|
||||
|
||||
func StringRequired(s string) bool {
|
||||
return s != ""
|
||||
}
|
||||
|
||||
func StringNoLeadingOrTrailingWhitespace(s string) bool {
|
||||
return s != "" && len(s) > 0 && s[0] != ' ' && s[len(s)-1] != ' '
|
||||
}
|
||||
|
||||
func StringContainsAt(s string) bool {
|
||||
for _, c := range s {
|
||||
if c == '@' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ReadString(message string, sv ...StringValidator) string {
|
||||
for {
|
||||
fmt.Print(message)
|
||||
var input string
|
||||
fmt.Scanln(&input)
|
||||
|
||||
if len(sv) == 0 {
|
||||
return input
|
||||
}
|
||||
|
||||
isValid := true
|
||||
for _, validator := range sv {
|
||||
if !validator(input) {
|
||||
isValid = false
|
||||
fmt.Println("Invalid input")
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if isValid {
|
||||
return input
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func ReadBool(message string) bool {
|
||||
for {
|
||||
fmt.Print(message + " (y/n) ")
|
||||
var input string
|
||||
fmt.Scanln(&input)
|
||||
|
||||
if input == "y" {
|
||||
return true
|
||||
} else if input == "n" {
|
||||
return false
|
||||
} else {
|
||||
fmt.Println("Invalid input")
|
||||
}
|
||||
}
|
||||
}
|
72
backend/app/generator/main.go
Normal file
72
backend/app/generator/main.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/git-web-template/backend/ent"
|
||||
"github.com/hay-kot/git-web-template/backend/internal/types"
|
||||
"github.com/hay-kot/git-web-template/backend/pkgs/automapper"
|
||||
"github.com/tkrajina/typescriptify-golang-structs/typescriptify"
|
||||
)
|
||||
|
||||
// generateMappers serialized the config file into a list of automapper struct
|
||||
func generateMappers() []automapper.AutoMapper {
|
||||
return []automapper.AutoMapper{
|
||||
{
|
||||
Package: "mapper",
|
||||
Prefix: "users",
|
||||
Name: "User Out",
|
||||
Schema: automapper.Schema{
|
||||
Type: types.UserOut{},
|
||||
Prefix: "types",
|
||||
},
|
||||
Model: automapper.Model{
|
||||
Type: ent.User{},
|
||||
Prefix: "ent",
|
||||
},
|
||||
Imports: []string{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func generateTypeScript() {
|
||||
// Configuration
|
||||
converter := typescriptify.New()
|
||||
converter.CreateInterface = true
|
||||
converter.ManageType(uuid.UUID{}, typescriptify.TypeOptions{TSType: "string"})
|
||||
converter.ManageType(time.Time{}, typescriptify.TypeOptions{TSType: "Date", TSTransform: "new Date(__VALUE__)"})
|
||||
|
||||
// General
|
||||
public := []any{
|
||||
// Base Types
|
||||
types.ApiSummary{},
|
||||
|
||||
// User Types
|
||||
types.UserOut{},
|
||||
types.UserCreate{},
|
||||
types.UserIn{},
|
||||
types.UserUpdate{},
|
||||
|
||||
// Auth Types
|
||||
types.LoginForm{},
|
||||
types.TokenResponse{},
|
||||
}
|
||||
|
||||
for i := 0; i < len(public); i++ {
|
||||
converter.Add(public[i])
|
||||
}
|
||||
|
||||
// Creation
|
||||
converter.ConvertToFile("./generated-types.ts")
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
automappers := generateMappers()
|
||||
conf := automapper.DefaultConf()
|
||||
|
||||
automapper.Generate(automappers, conf)
|
||||
|
||||
generateTypeScript()
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue