Loads of updates

This commit is contained in:
2021-06-22 22:52:48 +02:00
parent 8dc20a4eb0
commit ccafd60923
18 changed files with 1206 additions and 125 deletions

View File

@@ -5,7 +5,6 @@ import (
"strings"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)
// AccountCreate writes a user to database
@@ -15,38 +14,31 @@ func (d Db) AccountCreate(input AccountCreateInput) (CreatedAccount, error) {
_, err := d.DbPool.Exec(context.Background(), accountSQL, input.ID, input.Name, input.APIKey, input.Password)
if err != nil {
if strings.HasPrefix(err.Error(), "ERROR: duplicate key") {
log.WithFields(log.Fields{"name": input.Name}).Debug("Duplicate name in accounts database")
d.Log.Debug("Duplicate name in accounts database", "name", input.Name)
} else {
log.Error("Database error when trying to add account: " + err.Error())
d.Log.Error("Database error when trying to add account", "err", err.Error())
}
return CreatedAccount{}, err
}
log.WithFields(log.Fields{
"id": input.ID,
"name": input.Name,
}).Info("Added account to database")
d.Log.Info("Added account to database", "id", input.ID, "name", input.Name)
accountFieldsSQL := "INSERT INTO \"accountsFields\" (id, \"accountId\", name, value) VALUES($1,$2,$3,$4);"
for _, field := range input.Fields {
newFieldID, uuidErr := uuid.NewRandom()
if uuidErr != nil {
log.Fatal("Could not create new Uuid, err: " + uuidErr.Error())
d.Log.Fatal("Could not create new Uuid", "err", uuidErr.Error())
}
_, err := d.DbPool.Exec(context.Background(), accountFieldsSQL, newFieldID, input.ID, field.Name, field.Values)
if err != nil {
if strings.HasPrefix(err.Error(), "ERROR: duplicate key") {
log.Error("Database error when trying to account field: " + err.Error())
d.Log.Error("Database error when trying to account field", "err", err.Error())
}
}
log.WithFields(log.Fields{
"accountId": input.ID,
"fieldName": field.Name,
"fieldValues": field.Values,
}).Debug("Added account field")
d.Log.Debug("Added account field", "accountId", input.ID, "fieldName", field.Name, "fieldValues", field.Values)
}
return CreatedAccount{
@@ -58,12 +50,7 @@ func (d Db) AccountCreate(input AccountCreateInput) (CreatedAccount, error) {
// AccountGet fetches an account from the database
func (d Db) AccountGet(accountID string, APIKey string, Name string) (Account, error) {
logContext := log.WithFields(log.Fields{
"accountID": accountID,
"APIKey": len(APIKey),
})
logContext.Debug("Trying to get account")
d.Log.Debug("Trying to get account", "accountID", accountID, "len(APIKey)", len(APIKey))
var account Account
var searchParam string
@@ -82,18 +69,18 @@ func (d Db) AccountGet(accountID string, APIKey string, Name string) (Account, e
accountErr := d.DbPool.QueryRow(context.Background(), accountSQL, searchParam).Scan(&account.ID, &account.Created, &account.Name, &account.Password)
if accountErr != nil {
if accountErr.Error() == "no rows in result set" {
logContext.Debug("No account found")
d.Log.Debug("No account found", "accountID", accountID, "APIKey", len(APIKey))
return Account{}, accountErr
}
logContext.Error("Database error when fetching account, err: " + accountErr.Error())
d.Log.Error("Database error when fetching account", "err", accountErr.Error(), "accountID", accountID, "APIKey", len(APIKey))
return Account{}, accountErr
}
fieldsSQL := "SELECT name, value FROM \"accountsFields\" WHERE \"accountId\" = $1"
rows, fieldsErr := d.DbPool.Query(context.Background(), fieldsSQL, account.ID)
if fieldsErr != nil {
logContext.Error("Database error when fetching account fields, err: " + accountErr.Error())
d.Log.Error("Database error when fetching account fields", "err", accountErr.Error(), "accountID", accountID, "APIKey", len(APIKey))
return Account{}, fieldsErr
}
@@ -103,7 +90,7 @@ func (d Db) AccountGet(accountID string, APIKey string, Name string) (Account, e
var value []string
err := rows.Scan(&name, &value)
if err != nil {
logContext.Error("Could not get name or value from database row, err: " + err.Error())
d.Log.Error("Could not get name or value from database row", "err", err.Error(), "accountID", accountID, "APIKey", len(APIKey))
return Account{}, err
}
account.Fields[name] = value

View File

@@ -3,22 +3,19 @@ package db
import (
"context"
log "github.com/sirupsen/logrus"
"gitlab.larvit.se/power-plan/auth/src/utils"
)
// RenewalTokenCreate obtain a new renewal token
func (d Db) RenewalTokenCreate(accountID string) (string, error) {
logContext := log.WithFields(log.Fields{"accountID": accountID})
logContext.Debug("Creating new renewal token")
d.Log.Debug("Creating new renewal token", "accountID", accountID)
newToken := utils.RandString(60)
insertSQL := "INSERT INTO \"renewalTokens\" (\"accountId\",token) VALUES($1,$2);"
_, insertErr := d.DbPool.Exec(context.Background(), insertSQL, accountID, newToken)
if insertErr != nil {
logContext.Error("Could not insert into database table \"renewalTokens\", err: " + insertErr.Error())
d.Log.Error("Could not insert into database table \"renewalTokens\"", "err", insertErr.Error(), "accountID", accountID)
return "", insertErr
}
@@ -27,7 +24,7 @@ func (d Db) RenewalTokenCreate(accountID string) (string, error) {
// RenewalTokenGet checks if a valid renewal token exists in database
func (d Db) RenewalTokenGet(token string) (string, error) {
log.Debug("Trying to get a renewal token")
d.Log.Debug("Trying to get a renewal token")
sql := "SELECT \"accountId\" FROM \"renewalTokens\" WHERE exp >= now() AND token = $1"
@@ -38,7 +35,7 @@ func (d Db) RenewalTokenGet(token string) (string, error) {
return "", nil
}
log.Error("Database error when fetching renewal token, err: " + err.Error())
d.Log.Error("Database error when fetching renewal token", "err", err.Error())
return "", err
}
@@ -47,12 +44,12 @@ func (d Db) RenewalTokenGet(token string) (string, error) {
// RenewalTokenRm removes a renewal token from the database
func (d Db) RenewalTokenRm(token string) error {
log.Debug("Trying to remove a renewal token")
d.Log.Debug("Trying to remove a renewal token")
sql := "DELETE FROM \"renewalTokens\" WHERE token = $1"
_, err := d.DbPool.Exec(context.Background(), sql, token)
if err != nil {
log.Error("Database error when trying to remove token, err: " + err.Error())
d.Log.Error("Database error when trying to remove token", "err", err.Error())
return err
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/google/uuid"
"github.com/jackc/pgx/v4/pgxpool"
"go.uber.org/zap"
)
// Account is an account as represented in the database
@@ -41,4 +42,5 @@ type AccountCreateInput struct {
// Db struct
type Db struct {
DbPool *pgxpool.Pool
Log *zap.SugaredLogger
}

366
src/docs/docs.go Normal file
View File

@@ -0,0 +1,366 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag
package docs
import (
"bytes"
"encoding/json"
"strings"
"github.com/alecthomas/template"
"github.com/swaggo/swag"
)
var doc = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{.Description}}",
"title": "{{.Title}}",
"contact": {
"name": "Power Plan",
"url": "https://http://pwrpln.com/",
"email": "lilleman@larvit.se"
},
"license": {
"name": "MIT"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/account": {
"post": {
"description": "Create an account",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Create an account",
"operationId": "account-create",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/db.CreatedAccount"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"415": {
"description": "Unsupported Media Type",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
}
}
},
"/account/{id}": {
"get": {
"description": "Get account",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Get account",
"operationId": "get-account-by-id",
"parameters": [
{
"type": "string",
"description": "Account ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/db.Account"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"415": {
"description": "Unsupported Media Type",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
}
}
},
"/auth/api-key": {
"post": {
"description": "Authenticate account by API Key",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Authenticate account by API Key",
"operationId": "auth-account-by-api-key",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/db.Account"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"415": {
"description": "Unsupported Media Type",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
}
}
},
"/auth/password": {
"post": {
"description": "Authenticate account by Password",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Authenticate account by Password",
"operationId": "auth-account-by-password",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/db.Account"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"415": {
"description": "Unsupported Media Type",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
}
}
},
"/renew-token": {
"post": {
"description": "Renew token",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Renew token",
"operationId": "renew-token",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/db.Account"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"415": {
"description": "Unsupported Media Type",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
}
}
}
},
"definitions": {
"db.Account": {
"type": "object",
"properties": {
"created": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"db.CreatedAccount": {
"type": "object",
"properties": {
"apiKey": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"handlers.ResJSONError": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"field": {
"type": "string"
}
}
}
}
}`
type swaggerInfo struct {
Version string
Host string
BasePath string
Schemes []string
Title string
Description string
}
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = swaggerInfo{
Version: "0.1",
Host: "localhost:3000",
BasePath: "/",
Schemes: []string{},
Title: "JWT Auth API",
Description: "This is a tiny http API for auth. Register accounts, auth with api-key or name/password, renew JWT tokens...",
}
type s struct{}
func (s *s) ReadDoc() string {
sInfo := SwaggerInfo
sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1)
t, err := template.New("swagger_info").Funcs(template.FuncMap{
"marshal": func(v interface{}) string {
a, _ := json.Marshal(v)
return string(a)
},
}).Parse(doc)
if err != nil {
return doc
}
var tpl bytes.Buffer
if err := t.Execute(&tpl, sInfo); err != nil {
return doc
}
return tpl.String()
}
func init() {
swag.Register(swag.Name, &s{})
}

304
src/docs/swagger.json Normal file
View File

@@ -0,0 +1,304 @@
{
"swagger": "2.0",
"info": {
"description": "This is a tiny http API for auth. Register accounts, auth with api-key or name/password, renew JWT tokens...",
"title": "JWT Auth API",
"contact": {
"name": "Power Plan",
"url": "https://http://pwrpln.com/",
"email": "lilleman@larvit.se"
},
"license": {
"name": "MIT"
},
"version": "0.1"
},
"host": "localhost:3000",
"basePath": "/",
"paths": {
"/account": {
"post": {
"description": "Create an account",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Create an account",
"operationId": "account-create",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/db.CreatedAccount"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"415": {
"description": "Unsupported Media Type",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
}
}
},
"/account/{id}": {
"get": {
"description": "Get account",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Get account",
"operationId": "get-account-by-id",
"parameters": [
{
"type": "string",
"description": "Account ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/db.Account"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"415": {
"description": "Unsupported Media Type",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
}
}
},
"/auth/api-key": {
"post": {
"description": "Authenticate account by API Key",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Authenticate account by API Key",
"operationId": "auth-account-by-api-key",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/db.Account"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"415": {
"description": "Unsupported Media Type",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
}
}
},
"/auth/password": {
"post": {
"description": "Authenticate account by Password",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Authenticate account by Password",
"operationId": "auth-account-by-password",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/db.Account"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"415": {
"description": "Unsupported Media Type",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
}
}
},
"/renew-token": {
"post": {
"description": "Renew token",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Renew token",
"operationId": "renew-token",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/db.Account"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"415": {
"description": "Unsupported Media Type",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
}
}
}
},
"definitions": {
"db.Account": {
"type": "object",
"properties": {
"created": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"db.CreatedAccount": {
"type": "object",
"properties": {
"apiKey": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"handlers.ResJSONError": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"field": {
"type": "string"
}
}
}
}
}

203
src/docs/swagger.yaml Normal file
View File

@@ -0,0 +1,203 @@
basePath: /
definitions:
db.Account:
properties:
created:
type: string
fields:
additionalProperties:
items:
type: string
type: array
type: object
id:
type: string
name:
type: string
type: object
db.CreatedAccount:
properties:
apiKey:
type: string
id:
type: string
name:
type: string
type: object
handlers.ResJSONError:
properties:
error:
type: string
field:
type: string
type: object
host: localhost:3000
info:
contact:
email: lilleman@larvit.se
name: Power Plan
url: https://http://pwrpln.com/
description: This is a tiny http API for auth. Register accounts, auth with api-key
or name/password, renew JWT tokens...
license:
name: MIT
title: JWT Auth API
version: "0.1"
paths:
/account:
post:
consumes:
- application/json
description: Create an account
operationId: account-create
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/db.CreatedAccount'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/handlers.ResJSONError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/handlers.ResJSONError'
"415":
description: Unsupported Media Type
schema:
$ref: '#/definitions/handlers.ResJSONError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ResJSONError'
summary: Create an account
/account/{id}:
get:
consumes:
- application/json
description: Get account
operationId: get-account-by-id
parameters:
- description: Account ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/db.Account'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/handlers.ResJSONError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/handlers.ResJSONError'
"415":
description: Unsupported Media Type
schema:
$ref: '#/definitions/handlers.ResJSONError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ResJSONError'
summary: Get account
/auth/api-key:
post:
consumes:
- application/json
description: Authenticate account by API Key
operationId: auth-account-by-api-key
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/db.Account'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/handlers.ResJSONError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/handlers.ResJSONError'
"415":
description: Unsupported Media Type
schema:
$ref: '#/definitions/handlers.ResJSONError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ResJSONError'
summary: Authenticate account by API Key
/auth/password:
post:
consumes:
- application/json
description: Authenticate account by Password
operationId: auth-account-by-password
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/db.Account'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/handlers.ResJSONError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/handlers.ResJSONError'
"415":
description: Unsupported Media Type
schema:
$ref: '#/definitions/handlers.ResJSONError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ResJSONError'
summary: Authenticate account by Password
/renew-token:
post:
consumes:
- application/json
description: Renew token
operationId: renew-token
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/db.Account'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/handlers.ResJSONError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/handlers.ResJSONError'
"415":
description: Unsupported Media Type
schema:
$ref: '#/definitions/handlers.ResJSONError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ResJSONError'
summary: Renew token
swagger: "2.0"

View File

@@ -4,12 +4,19 @@ import (
"github.com/gofiber/fiber/v2"
)
// Hello handler
func (h Handlers) Hello(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
}
// AccountGet handler
// AccountGet godoc
// @Summary Get account
// @Description Get account
// @ID get-account-by-id
// @Accept json
// @Produce json
// @Param id path string true "Account ID"
// @Success 200 {object} db.Account
// @Failure 401 {object} ResJSONError
// @Failure 403 {object} ResJSONError
// @Failure 415 {object} ResJSONError
// @Failure 500 {object} ResJSONError
// @Router /account/{id} [get]
func (h Handlers) AccountGet(c *fiber.Ctx) error {
accountID := c.Params("accountID")
// logContext := log.WithFields(log.Fields{"accountID": accountID})

View File

@@ -7,7 +7,6 @@ import (
jwt "github.com/dgrijalva/jwt-go"
"github.com/gofiber/fiber/v2"
log "github.com/sirupsen/logrus"
"gitlab.larvit.se/power-plan/auth/src/db"
)
@@ -26,13 +25,13 @@ func (h Handlers) returnTokens(account db.Account, c *fiber.Ctx) error {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(h.JwtKey)
if err != nil {
log.Error("Could not create token string, err: " + err.Error())
h.Log.Error("Could not create token string", "err", err.Error())
return c.Status(500).JSON([]ResJSONError{{Error: "Could not create JWT token string"}})
}
renewalToken, renewalTokenErr := h.Db.RenewalTokenCreate(account.ID.String())
if renewalTokenErr != nil {
log.Error("Could not create renewal token, err: " + renewalTokenErr.Error())
h.Log.Error("Could not create renewal token", "err", renewalTokenErr.Error())
return c.Status(500).JSON([]ResJSONError{{Error: "Could not create renewal token"}})
}
@@ -43,15 +42,14 @@ func (h Handlers) returnTokens(account db.Account, c *fiber.Ctx) error {
}
func (h Handlers) parseJWT(JWT string) (Claims, error) {
logContext := log.WithFields(log.Fields{"JWT": JWT})
logContext.Trace("Parsing JWT")
h.Log.Debug("Parsing JWT", "JWT", JWT)
JWT = strings.TrimPrefix(JWT, "bearer ") // Since the Authorization header should always start with "bearer "
logContext.WithFields(log.Fields{"TrimmedJWT": JWT}).Trace("JWT trimmed")
trimmedJWT := strings.TrimPrefix(JWT, "bearer ") // Since the Authorization header should always start with "bearer "
h.Log.Debug("JWT trimmed", "JWT", JWT, "trimmedJWT", trimmedJWT)
claims := &Claims{}
token, err := jwt.ParseWithClaims(JWT, claims, func(token *jwt.Token) (interface{}, error) {
token, err := jwt.ParseWithClaims(trimmedJWT, claims, func(token *jwt.Token) (interface{}, error) {
return h.JwtKey, nil
})
if err != nil {
@@ -75,7 +73,7 @@ func (h Handlers) parseHeaders(c *fiber.Ctx) map[string]string {
lineParts := strings.Split(line, ": ")
if len(lineParts) == 1 {
log.WithFields(log.Fields{"line": line}).Trace("Ignoring header line")
h.Log.Debug("Ignoring header line", "line", line)
} else {
headersMap[lineParts[0]] = lineParts[1]
}

View File

@@ -2,15 +2,11 @@ package handlers
import (
"github.com/gofiber/fiber/v2"
log "github.com/sirupsen/logrus"
)
// Log all requests
func (h Handlers) Log(c *fiber.Ctx) error {
log.WithFields(log.Fields{
"method": c.Method(),
"url": c.OriginalURL(),
}).Debug("http request")
func (h Handlers) LogReq(c *fiber.Ctx) error {
h.Log.Debug("http request", "method", c.Method(), "url", c.OriginalURL())
c.Next()
return nil
@@ -22,7 +18,7 @@ func (h Handlers) RequireJSON(c *fiber.Ctx) error {
contentType := string(c.Request().Header.ContentType())
if contentType != "application/json" && contentType != "" {
log.WithFields(log.Fields{"content-type": contentType}).Debug("Invalid content-type in request")
h.Log.Debug("Invalid content-type in request", "content-type", contentType)
return c.Status(415).JSON([]ResJSONError{{Error: "Invalid content-type"}})
}

View File

@@ -3,15 +3,24 @@ package handlers
import (
"strings"
log "github.com/sirupsen/logrus"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"gitlab.larvit.se/power-plan/auth/src/db"
"gitlab.larvit.se/power-plan/auth/src/utils"
)
// AccountCreate creates a new account
// AccountCreate godoc
// @Summary Create an account
// @Description Create an account
// @ID account-create
// @Accept json
// @Produce json
// @Success 200 {object} db.CreatedAccount
// @Failure 401 {object} ResJSONError
// @Failure 403 {object} ResJSONError
// @Failure 415 {object} ResJSONError
// @Failure 500 {object} ResJSONError
// @Router /account [post]
func (h Handlers) AccountCreate(c *fiber.Ctx) error {
authErr := h.RequireAdminRole(c)
if authErr != nil {
@@ -44,12 +53,12 @@ func (h Handlers) AccountCreate(c *fiber.Ctx) error {
newAccountID, uuidErr := uuid.NewRandom()
if uuidErr != nil {
log.Fatal("Could not create new Uuid, err: " + uuidErr.Error())
h.Log.Fatal("Could not create new Uuid", "err", uuidErr.Error())
}
hashedPwd, pwdErr := utils.HashPassword(accountInput.Password)
if pwdErr != nil {
log.Fatal("Could not hash password, err: " + pwdErr.Error())
h.Log.Fatal("Could not hash password", "err", pwdErr.Error())
}
createdAccount, err := h.Db.AccountCreate(db.AccountCreateInput{
@@ -70,7 +79,18 @@ func (h Handlers) AccountCreate(c *fiber.Ctx) error {
return c.Status(201).JSON(createdAccount)
}
// AccountAuthAPIKey auths an APIKey
// AccountAuthAPIKey godoc
// @Summary Authenticate account by API Key
// @Description Authenticate account by API Key
// @ID auth-account-by-api-key
// @Accept json
// @Produce json
// @Success 200 {object} db.Account
// @Failure 401 {object} ResJSONError
// @Failure 403 {object} ResJSONError
// @Failure 415 {object} ResJSONError
// @Failure 500 {object} ResJSONError
// @Router /auth/api-key [post]
func (h Handlers) AccountAuthAPIKey(c *fiber.Ctx) error {
inputAPIKey := string(c.Request().Body())
inputAPIKey = inputAPIKey[1 : len(inputAPIKey)-1]
@@ -80,14 +100,25 @@ func (h Handlers) AccountAuthAPIKey(c *fiber.Ctx) error {
if accountErr.Error() == "no rows in result set" {
return c.Status(403).JSON([]ResJSONError{{Error: "Invalid credentials"}})
}
log.Error("Something went wrong when trying to fetch account")
h.Log.Error("Something went wrong when trying to fetch account", "err", accountErr.Error())
return c.Status(500).JSON([]ResJSONError{{Error: "Something went wrong when trying to fetch account"}})
}
return h.returnTokens(resolvedAccount, c)
}
// AccountAuthPassword auths a name/password pair
// AccountAuthPassword godoc
// @Summary Authenticate account by Password
// @Description Authenticate account by Password
// @ID auth-account-by-password
// @Accept json
// @Produce json
// @Success 200 {object} db.Account
// @Failure 401 {object} ResJSONError
// @Failure 403 {object} ResJSONError
// @Failure 415 {object} ResJSONError
// @Failure 500 {object} ResJSONError
// @Router /auth/password [post]
func (h Handlers) AccountAuthPassword(c *fiber.Ctx) error {
type AuthInput struct {
Name string `json:"name"`
@@ -115,8 +146,19 @@ func (h Handlers) AccountAuthPassword(c *fiber.Ctx) error {
return h.returnTokens(resolvedAccount, c)
}
// TokenRenew creates a new renewal token and JWT from an old renewal token
func (h Handlers) TokenRenew(c *fiber.Ctx) error {
// RenewToken godoc
// @Summary Renew token
// @Description Renew token
// @ID renew-token
// @Accept json
// @Produce json
// @Success 200 {object} db.Account
// @Failure 401 {object} ResJSONError
// @Failure 403 {object} ResJSONError
// @Failure 415 {object} ResJSONError
// @Failure 500 {object} ResJSONError
// @Router /renew-token [post]
func (h Handlers) RenewToken(c *fiber.Ctx) error {
inputToken := string(c.Request().Body())
inputToken = inputToken[1 : len(inputToken)-1]
@@ -132,13 +174,14 @@ func (h Handlers) TokenRenew(c *fiber.Ctx) error {
if accountErr.Error() == "no rows in result set" {
return c.Status(500).JSON([]ResJSONError{{Error: "Database missmatch. Token found, but account is missing."}})
}
log.Error("Something went wrong when trying to fetch account")
h.Log.Error("Something went wrong when trying to fetch account", "err", accountErr.Error())
return c.Status(500).JSON([]ResJSONError{{Error: "Something went wrong when trying to fetch account"}})
}
rmErr := h.Db.RenewalTokenRm(inputToken)
if rmErr != nil {
return c.Status(500).JSON([]ResJSONError{{Error: "Could not remove old token, err: " + rmErr.Error()}})
h.Log.Error("Something went wrong when trying to fetch account", "err", rmErr.Error())
return c.Status(500).JSON([]ResJSONError{{Error: "Could not remove old token"}})
}
return h.returnTokens(resolvedAccount, c)

View File

@@ -3,6 +3,7 @@ package handlers
import (
jwt "github.com/dgrijalva/jwt-go"
"gitlab.larvit.se/power-plan/auth/src/db"
"go.uber.org/zap"
)
// Claims is the JWT struct
@@ -17,6 +18,7 @@ type Claims struct {
type Handlers struct {
Db db.Db
JwtKey []byte
Log *zap.SugaredLogger
}
// ResJSONError is an error field that is used in JSON error responses

View File

@@ -5,19 +5,25 @@ import (
"os"
"strings"
swagger "github.com/arsmn/fiber-swagger/v2"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/joho/godotenv"
log "github.com/sirupsen/logrus"
"gitlab.larvit.se/power-plan/auth/src/db"
h "gitlab.larvit.se/power-plan/auth/src/handlers"
"gitlab.larvit.se/power-plan/auth/src/utils"
"go.uber.org/zap"
// docs are generated by Swag CLI, you have to import them.
_ "gitlab.larvit.se/power-plan/auth/src/docs"
)
func createAdminAccount(Db db.Db) {
// Don't put in utils, because it creates import cycle with db... just left it here for now
func createAdminAccount(Db db.Db, log *zap.SugaredLogger) {
adminAccountID, uuidErr := uuid.NewRandom()
if uuidErr != nil {
log.Fatal("Could not create new Uuid, err: " + uuidErr.Error())
log.Fatal("Could not create new Uuid", "err", uuidErr.Error())
}
_, adminAccountErr := Db.AccountCreate(db.AccountCreateInput{
ID: adminAccountID,
@@ -29,20 +35,30 @@ func createAdminAccount(Db db.Db) {
if adminAccountErr != nil && strings.HasPrefix(adminAccountErr.Error(), "ERROR: duplicate key") {
log.Info("Admin account already created, nothing written to database")
} else if adminAccountErr != nil {
log.Fatal("Could not create admin account, err: " + adminAccountErr.Error())
log.Fatal("Could not create admin account", "err", adminAccountErr.Error())
}
}
// @title JWT Auth API
// @version 0.1
// @description This is a tiny http API for auth. Register accounts, auth with api-key or name/password, renew JWT tokens...
// @contact.name Power Plan
// @contact.url https://http://pwrpln.com/
// @contact.email lilleman@larvit.se
// @license.name MIT
// @host localhost:3000
// @BasePath /
func main() {
log := utils.GetLog()
err := godotenv.Load()
if err != nil {
log.Warn("Error loading .env file, this could be ok if the env file does not exist")
log.Warn("Error loading .env file, this could be ok if the env file does not exist", "err", err.Error())
}
// Add this line for logging filename and line number!
// log.SetReportCaller(true)
log.SetLevel(log.DebugLevel)
if os.Getenv("JWT_SHARED_SECRET") == "changeMe" {
log.Fatal("You must change JWT_SHARED_SECRET in .env")
}
@@ -53,7 +69,7 @@ func main() {
dbPool, err := pgxpool.Connect(context.Background(), os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal("Failed to open DB connection: ", err)
log.Fatal("Failed to open DB connection", "err", err.Error())
} else {
log.Info("Connected to PostgreSQL database")
}
@@ -61,28 +77,31 @@ func main() {
app := fiber.New()
Db := db.Db{DbPool: dbPool}
handlers := h.Handlers{Db: Db, JwtKey: jwtKey}
Db := db.Db{DbPool: dbPool, Log: log}
handlers := h.Handlers{Db: Db, JwtKey: jwtKey, Log: log}
createAdminAccount(Db)
createAdminAccount(Db, log)
// Log all requests
app.Use(handlers.Log)
app.Use(handlers.LogReq)
// Always require application/json
app.Use(handlers.RequireJSON)
app.Get("/", handlers.Hello)
app.Get("/", func(c *fiber.Ctx) error { return c.Redirect("/swagger/index.html") })
app.Get("/swagger", func(c *fiber.Ctx) error { return c.Redirect("/swagger/index.html") })
app.Get("/swagger/*", swagger.Handler)
app.Get("/account/:accountID", handlers.AccountGet)
app.Post("/account", handlers.AccountCreate)
app.Post("/auth/api-key", handlers.AccountAuthAPIKey)
app.Post("/auth/password", handlers.AccountAuthPassword)
app.Post("/renew-token", handlers.TokenRenew)
app.Post("/renew-token", handlers.RenewToken)
log.WithFields(log.Fields{"WEB_BIND_HOST": os.Getenv("WEB_BIND_HOST")}).Info("Trying to start web server")
log.Info("Trying to start web server", "WEB_BIND_HOST", os.Getenv("WEB_BIND_HOST"))
if err := app.Listen(os.Getenv("WEB_BIND_HOST")); err != nil {
log.Fatal(err)
log.Fatal("Could not start web server", "err", err.Error())
}
log.Info("Webb server closed, shutting down")

View File

@@ -1,10 +1,13 @@
package utils
import (
"log"
"math/rand"
"strings"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/crypto/bcrypt"
)
@@ -48,3 +51,31 @@ func RandString(n int) string {
return sb.String()
}
func SyslogTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
loc, _ := time.LoadLocation("") // "" == UTC
t = t.In(loc)
enc.AppendString(t.Format("2006-01-02 15:04:05"))
}
func GetLog() *zap.SugaredLogger {
cfg := zap.NewProductionConfig()
cfg.Development = true
cfg.DisableCaller = false
cfg.DisableStacktrace = false
cfg.Encoding = "console" // "console" or "json"
cfg.EncoderConfig.EncodeTime = SyslogTimeEncoder
cfg.OutputPaths = []string{"stdout"}
cfg.ErrorOutputPaths = []string{"stderr"}
cfg.Level.SetLevel(zap.DebugLevel)
logger, err := cfg.Build()
if err != nil {
log.Panicf("Could not build logger, err: %v", err)
}
defer logger.Sync() // flushes buffer, if any
log := logger.Sugar()
return log
}