Adding models, DB access, signup, login

* Created Base, Auth, User and Admin models
* Added skeleton API structure containing: User signup, User & Admin
login, authorized zones, ping tests
* Simple user signup functional
* Skeleton user login functional, no means to verify as of yet
* Added POSTMAN file
This commit is contained in:
🐙PiperYxzzy
2022-04-29 23:50:55 +02:00
parent b74158a7a5
commit 47ac0cdc07
10 changed files with 508 additions and 2 deletions

4
.gitignore vendored
View File

@@ -13,3 +13,7 @@
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
# Other
prepack.db
test_prepack.db

View File

@@ -0,0 +1,136 @@
{
"info": {
"_postman_id": "6485c58d-0675-4f5d-9eed-4c2ecd8174ae",
"name": "Prepack",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "V1 Doot",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "localhost:9091/v1/doot",
"host": [
"localhost"
],
"port": "9091",
"path": [
"v1",
"doot"
]
}
},
"response": []
},
{
"name": "V1 Secured Doot",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "localhost:9091/v1/sec/doot",
"host": [
"localhost"
],
"port": "9091",
"path": [
"v1",
"sec",
"doot"
]
}
},
"response": []
},
{
"name": "V1 Admin Doot",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "localhost:9091/v1/adm/doot",
"host": [
"localhost"
],
"port": "9091",
"path": [
"v1",
"adm",
"doot"
]
}
},
"response": []
},
{
"name": "V1 Signup",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"userkey\": \"NewUser\",\n \"password\": \"NewPass\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "localhost:9091/v1/signup",
"host": [
"localhost"
],
"port": "9091",
"path": [
"v1",
"signup"
],
"query": [
{
"key": "userkey",
"value": "NewUser",
"disabled": true
},
{
"key": "password",
"value": "NewPass",
"disabled": true
}
]
}
},
"response": []
},
{
"name": "V1 User Login",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"userkey\": \"NewUser\",\n \"password\": \"NewPass\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "localhost:9091/v1/login",
"host": [
"localhost"
],
"port": "9091",
"path": [
"v1",
"login"
]
}
},
"response": []
}
]
}

58
database/database.go Normal file
View File

@@ -0,0 +1,58 @@
package database
import (
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Database struct {
*gorm.DB
}
var Db *gorm.DB
func Init() *gorm.DB {
db, err := gorm.Open(sqlite.Open("prepack.db"), &gorm.Config{})
if err != nil {
panic(err)
}
//TODO GORM settings
database, err := db.DB()
if err != nil {
panic(err)
}
database.SetMaxIdleConns(10)
database.SetMaxOpenConns(50)
database.SetConnMaxLifetime(time.Minute * 30)
Db = db
return db
}
func InitTestDb() *gorm.DB {
db, err := gorm.Open(sqlite.Open("test_prepack.db"), &gorm.Config{})
if err != nil {
panic(err)
}
//TODO GORM settings
database, err := db.DB()
if err != nil {
panic(err)
}
database.SetMaxIdleConns(10)
database.SetMaxOpenConns(50)
database.SetConnMaxLifetime(time.Minute * 30)
Db = db
return db
}
func GetDb() *gorm.DB {
return Db
}

10
go.mod
View File

@@ -2,16 +2,22 @@ module github.com/yxzzy-wtf/gin-gonic-prepack
go 1.18 go 1.18
require github.com/gin-gonic/gin v1.7.7
require ( require (
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.7.7 // indirect
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.1 // indirect github.com/go-playground/validator/v10 v10.10.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/ugorji/go/codec v1.2.7 // indirect
@@ -20,4 +26,6 @@ require (
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gorm.io/driver/sqlite v1.3.2 // indirect
gorm.io/gorm v1.23.5 // indirect
) )

16
go.sum
View File

@@ -15,12 +15,21 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.10.1 h1:uA0+amWMiglNZKZ9FJRKUAe9U3RX91eVn1JYXMWt7ig= github.com/go-playground/validator/v10 v10.10.1 h1:uA0+amWMiglNZKZ9FJRKUAe9U3RX91eVn1JYXMWt7ig=
github.com/go-playground/validator/v10 v10.10.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-playground/validator/v10 v10.10.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@@ -36,6 +45,8 @@ github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ic
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -96,3 +107,8 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.3.2 h1:nWTy4cE52K6nnMhv23wLmur9Y3qWbZvOBz+V4PrGAxg=
gorm.io/driver/sqlite v1.3.2/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U=
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=

164
main.go
View File

@@ -1,7 +1,169 @@
package main package main
import _ "github.com/gin-gonic/gin" import (
"fmt"
"log"
"net/http"
"time"
"github.com/yxzzy-wtf/gin-gonic-prepack/database"
"github.com/yxzzy-wtf/gin-gonic-prepack/models"
"github.com/gin-gonic/gin"
_ "github.com/golang-jwt/jwt"
"gorm.io/gorm"
)
func Migrate(g *gorm.DB) {
g.AutoMigrate(&models.User{})
g.AutoMigrate(&models.Admin{})
}
func main() { func main() {
db := database.Init()
Migrate(db)
r := gin.Default()
v1 := r.Group("/v1")
// Ping functionality
v1.GET("/doot", doot())
// Standard user login
v1.POST("/signup", userSignup())
v1.POST("/login", userLogin())
v1Sec := v1.Group("/sec", userAuth())
v1Sec.GET("/doot", doot())
// Administrative login
v1.POST("/admin", adminLogin())
v1Admin := v1.Group("/adm", adminAuth())
v1Admin.GET("/doot", doot())
// Start server
if err := http.ListenAndServe(":9091", r); err != nil {
log.Fatal(err)
}
}
func doot() gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusOK, map[string]string{"snoot": "dooted"})
}
}
type login struct {
UserKey string `json:"userkey" binding:"required"`
Password string `json:"password" binding:"required"`
TwoFactor string `json:"twofactorcode"`
}
type signup struct {
UserKey string `json:"userkey" binding:"required"`
Password string `json:"password" binding:"required"`
}
type failmsg struct {
Reason string `json:"reason"`
}
const JwtHeader = "jwt"
const ServicePath = "TODOPATH"
const ServiceDomain = "TODODOMAIN"
func userLogin() gin.HandlerFunc {
return func(c *gin.Context) {
var loginVals login
if err := c.ShouldBind(&loginVals); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, failmsg{"Requires username and password"})
}
u := models.User{}
if err := u.ByEmail(loginVals.UserKey); err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
err := u.CheckPassword(loginVals.Password)
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
err = u.ValidateTwoFactor(loginVals.TwoFactor, time.Now())
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{err.Error()})
return
}
if !u.Verified {
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{"not yet verified"})
return
}
jwt, maxAge := u.GetJwt()
c.SetCookie(JwtHeader, jwt, maxAge, ServicePath, ServiceDomain, true, true)
}
}
func userSignup() gin.HandlerFunc {
return func(c *gin.Context) {
var signupVals signup
if err := c.ShouldBind(&signupVals); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, failmsg{"Requires username and password"})
return
}
u := models.User{
Email: signupVals.UserKey,
}
if err := u.SetPassword(signupVals.Password); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, failmsg{"Bad password"})
return
}
if err := database.Db.Model(&u).Create(&u).Error; err != nil {
if err.Error() == "UNIQUE constraint failed: users.email" {
c.AbortWithStatusJSON(http.StatusInternalServerError, failmsg{"already exists"})
} else {
fmt.Println(fmt.Errorf("error: %w", err))
c.AbortWithStatus(http.StatusInternalServerError)
}
return
}
c.JSON(http.StatusOK, map[string]string{"id": u.Uid.String()})
}
}
func adminLogin() gin.HandlerFunc {
return func(c *gin.Context) {
}
}
func userAuth() gin.HandlerFunc {
return func(c *gin.Context) {
jwt := c.GetHeader(JwtHeader)
if jwt == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{"Requires `" + jwt + "` header"})
return
}
}
}
func adminAuth() gin.HandlerFunc {
return func(c *gin.Context) {
jwt := c.GetHeader(JwtHeader)
if jwt == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{"Requires `" + jwt + "` header"})
return
}
c.AbortWithStatus(http.StatusUnauthorized)
}
} }

24
models/admin.go Normal file
View File

@@ -0,0 +1,24 @@
package models
import (
"errors"
"github.com/yxzzy-wtf/gin-gonic-prepack/database"
)
type Admin struct {
Auth
Email string
}
func (a *Admin) GetJwt() (string, int) {
return "", 0
}
func (a *Admin) ByEmail(email string) error {
if err := database.Db.Where("email = ?", email).First(&a).Error; err != nil {
return errors.New("not found")
}
return nil
}

43
models/auth.go Normal file
View File

@@ -0,0 +1,43 @@
package models
import (
"errors"
"time"
"golang.org/x/crypto/bcrypt"
)
type Auth struct {
Base
PasswordHash string
TwoFactorSecret string
TwoFactorRecovery string
Verified bool
}
func (a *Auth) SetPassword(pass string) error {
passHash, _ := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
a.PasswordHash = string(passHash)
return nil
}
func (a *Auth) CheckPassword(pass string) error {
return bcrypt.CompareHashAndPassword([]byte(a.PasswordHash), []byte(pass))
}
func (a *Auth) ValidateTwoFactor(tfCode string, stamp time.Time) error {
if tfCode == "" && a.TwoFactorSecret != "" {
return errors.New("requires 2FA")
} else if tfCode == "" && a.TwoFactorSecret == "" {
return nil
}
//TODO two factor
if len(tfCode) == 6 {
// Test 2FA
return errors.New("2FA invalid")
} else {
// May be a renewal code
return errors.New("unlock invalid")
}
}

31
models/base.go Normal file
View File

@@ -0,0 +1,31 @@
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Base struct {
Uid uuid.UUID `gorm:"type:uuid;primary_key;"`
Created time.Time
Updated time.Time
Deleted time.Time `sql:"index"`
Tenant uuid.UUID
}
func (b *Base) BeforeCreate(scope *gorm.DB) error {
b.Uid = uuid.New()
b.Created = time.Now()
return nil
}
func (b *Base) BeforeSave(tx *gorm.DB) error {
b.Updated = time.Now()
return nil
}
func (b *Base) Delete() {
b.Deleted = time.Now()
}

24
models/user.go Normal file
View File

@@ -0,0 +1,24 @@
package models
import (
"errors"
"github.com/yxzzy-wtf/gin-gonic-prepack/database"
)
type User struct {
Auth
Email string `gorm:"unique"`
}
func (u *User) GetJwt() (string, int) {
return "", 0
}
func (u *User) ByEmail(email string) error {
if err := database.Db.Where("email = ?", email).First(&u).Error; err != nil {
return errors.New("not found")
}
return nil
}