Login, JWT and auth overstructure
* Signup -> Login -> JWT-Doot flow now works for users * Administrators cannot currently sign up for obvious reasons * Segmented the main.go methods into a core controller package
This commit is contained in:
193
controllers/core/core.go
Normal file
193
controllers/core/core.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/yxzzy-wtf/gin-gonic-prepack/database"
|
||||||
|
"github.com/yxzzy-wtf/gin-gonic-prepack/models"
|
||||||
|
"github.com/yxzzy-wtf/gin-gonic-prepack/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if err, returnErr := u.Login(loginVals.Password, loginVals.TwoFactor); err != nil {
|
||||||
|
if returnErr {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{err.Error()})
|
||||||
|
} else {
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt, maxAge := u.GetJwt()
|
||||||
|
c.SetCookie(JwtHeader, jwt, maxAge, ServicePath, ServiceDomain, true, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminLogin() 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"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginVals.TwoFactor == "" {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{"admin access requires 2FA"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a := models.Admin{}
|
||||||
|
if err := a.ByEmail(loginVals.UserKey); err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err, returnErr := a.Login(loginVals.Password, loginVals.TwoFactor); err != nil {
|
||||||
|
if returnErr {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{err.Error()})
|
||||||
|
} else {
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt, maxAge := a.GetJwt()
|
||||||
|
c.SetCookie(JwtHeader, jwt, maxAge, ServicePath, ServiceDomain, true, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func genericAuth(expectedRole string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
tokenStr := c.GetHeader(JwtHeader)
|
||||||
|
if tokenStr == "" {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{"requires `" + JwtHeader + "` header"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := parseJwt(tokenStr, models.UserHmac)
|
||||||
|
if err != nil {
|
||||||
|
if strings.HasPrefix(err.Error(), "token ") || err.Error() == "signature is invalid" {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{err.Error()})
|
||||||
|
} else {
|
||||||
|
fmt.Println(err)
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, failmsg{"something went wrong"})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if claims["role"] != expectedRole {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{"wrong access role"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, err := uuid.Parse(claims["sub"].(string))
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{"cannot extract sub"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("principal", util.PrincipalInfo{Uid: uid, Role: expectedRole})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserAuth() gin.HandlerFunc {
|
||||||
|
return genericAuth("user")
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminAuth() gin.HandlerFunc {
|
||||||
|
return genericAuth("admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseJwt(tokenStr string, hmac []byte) (jwt.MapClaims, error) {
|
||||||
|
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("bad signing method %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return hmac, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||||
|
return claims, nil
|
||||||
|
} else {
|
||||||
|
return jwt.MapClaims{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Doot() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
piCtx, exists := c.Get("principal")
|
||||||
|
if exists {
|
||||||
|
pi := piCtx.(util.PrincipalInfo)
|
||||||
|
dooter := pi.Role + ":" + pi.Uid.String()
|
||||||
|
c.JSON(http.StatusOK, map[string]string{"snoot": "dooted by " + dooter})
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, map[string]string{"snoot": "dooted"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
158
main.go
158
main.go
@@ -1,15 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/yxzzy-wtf/gin-gonic-prepack/controllers/core"
|
||||||
"github.com/yxzzy-wtf/gin-gonic-prepack/database"
|
"github.com/yxzzy-wtf/gin-gonic-prepack/database"
|
||||||
"github.com/yxzzy-wtf/gin-gonic-prepack/models"
|
"github.com/yxzzy-wtf/gin-gonic-prepack/models"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
_ "github.com/golang-jwt/jwt"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,162 +25,23 @@ func main() {
|
|||||||
v1 := r.Group("/v1")
|
v1 := r.Group("/v1")
|
||||||
|
|
||||||
// Ping functionality
|
// Ping functionality
|
||||||
v1.GET("/doot", doot())
|
v1.GET("/doot", core.Doot())
|
||||||
|
|
||||||
// Standard user login
|
// Standard user login
|
||||||
v1.POST("/signup", userSignup())
|
v1.POST("/signup", core.UserSignup())
|
||||||
v1.POST("/login", userLogin())
|
v1.POST("/login", core.UserLogin())
|
||||||
v1Sec := v1.Group("/sec", userAuth())
|
v1Sec := v1.Group("/sec", core.UserAuth())
|
||||||
|
|
||||||
v1Sec.GET("/doot", doot())
|
v1Sec.GET("/doot", core.Doot())
|
||||||
|
|
||||||
// Administrative login
|
// Administrative login
|
||||||
v1.POST("/admin", adminLogin())
|
v1.POST("/admin", core.AdminLogin())
|
||||||
v1Admin := v1.Group("/adm", adminAuth())
|
v1Admin := v1.Group("/adm", core.AdminAuth())
|
||||||
|
|
||||||
v1Admin.GET("/doot", doot())
|
v1Admin.GET("/doot", core.Doot())
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
if err := http.ListenAndServe(":9091", r); err != nil {
|
if err := http.ListenAndServe(":9091", r); err != nil {
|
||||||
log.Fatal(err)
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if err, returnErr := u.Login(loginVals.Password, loginVals.TwoFactor); err != nil {
|
|
||||||
if returnErr {
|
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{err.Error()})
|
|
||||||
} else {
|
|
||||||
c.AbortWithStatus(http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
var loginVals login
|
|
||||||
if err := c.ShouldBind(&loginVals); err != nil {
|
|
||||||
c.AbortWithStatusJSON(http.StatusBadRequest, failmsg{"requires username and password"})
|
|
||||||
}
|
|
||||||
|
|
||||||
if loginVals.TwoFactor == "" {
|
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{"admin access requires 2FA"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
a := models.Admin{}
|
|
||||||
if err := a.ByEmail(loginVals.UserKey); err != nil {
|
|
||||||
c.AbortWithStatus(http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err, returnErr := a.Login(loginVals.Password, loginVals.TwoFactor); err != nil {
|
|
||||||
if returnErr {
|
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{err.Error()})
|
|
||||||
} else {
|
|
||||||
c.AbortWithStatus(http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt, maxAge := a.GetJwt()
|
|
||||||
c.SetCookie(JwtHeader, jwt, maxAge, ServicePath, ServiceDomain, true, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func userAuth() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
jwt := c.GetHeader(JwtHeader)
|
|
||||||
if jwt == "" {
|
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{"requires `" + JwtHeader + "` header"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.AbortWithStatus(http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func adminAuth() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
jwt := c.GetHeader(JwtHeader)
|
|
||||||
if jwt == "" {
|
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, failmsg{"requires `" + JwtHeader + "` header"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.AbortWithStatus(http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
type Admin struct {
|
type Admin struct {
|
||||||
Auth
|
Auth
|
||||||
Email string
|
Email string `gorm:"unique" sql:"index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminJwtDuration = time.Hour * 2
|
const adminJwtDuration = time.Hour * 2
|
||||||
@@ -19,11 +19,10 @@ const adminJwtDuration = time.Hour * 2
|
|||||||
var adminHmac = util.GenerateHmac()
|
var adminHmac = util.GenerateHmac()
|
||||||
|
|
||||||
func (a *Admin) GetJwt() (string, int) {
|
func (a *Admin) GetJwt() (string, int) {
|
||||||
exp := time.Now().Add(adminJwtDuration)
|
|
||||||
j := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
j := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
"sub": a.Uid.String(),
|
"sub": a.Uid.String(),
|
||||||
"iat": time.Now(),
|
"iat": time.Now().Unix(),
|
||||||
"exp": exp,
|
"exp": time.Now().Add(adminJwtDuration).Unix(),
|
||||||
"role": "admin",
|
"role": "admin",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -12,7 +13,7 @@ type Base struct {
|
|||||||
Created time.Time
|
Created time.Time
|
||||||
Updated time.Time
|
Updated time.Time
|
||||||
Deleted time.Time `sql:"index"`
|
Deleted time.Time `sql:"index"`
|
||||||
Tenant uuid.UUID
|
Tenant uuid.UUID `sql:"index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Base) BeforeCreate(scope *gorm.DB) error {
|
func (b *Base) BeforeCreate(scope *gorm.DB) error {
|
||||||
@@ -21,7 +22,11 @@ func (b *Base) BeforeCreate(scope *gorm.DB) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Base) BeforeSave(tx *gorm.DB) error {
|
func (b *Base) BeforeSave(scope *gorm.DB) error {
|
||||||
|
if b.Tenant == uuid.Nil {
|
||||||
|
return errors.New("cannot save an untenanted object")
|
||||||
|
}
|
||||||
|
|
||||||
b.Updated = time.Now()
|
b.Updated = time.Now()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,24 +11,23 @@ import (
|
|||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Auth
|
Auth
|
||||||
Email string `gorm:"unique"`
|
Email string `gorm:"unique" sql:"index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const userJwtDuration = time.Hour * 24
|
const userJwtDuration = time.Hour * 24
|
||||||
|
|
||||||
var userHmac = util.GenerateHmac()
|
var UserHmac = util.GenerateHmac()
|
||||||
|
|
||||||
func (u *User) GetJwt() (string, int) {
|
func (u *User) GetJwt() (string, int) {
|
||||||
exp := time.Now().Add(userJwtDuration)
|
|
||||||
j := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
j := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
"sub": u.Uid.String(),
|
"sub": u.Uid.String(),
|
||||||
"iat": time.Now(),
|
"iat": time.Now().Unix(),
|
||||||
"exp": exp,
|
"exp": time.Now().Add(userJwtDuration).Unix(),
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"tid": u.Tenant.String(),
|
"tid": u.Tenant.String(),
|
||||||
})
|
})
|
||||||
|
|
||||||
jstr, err := j.SignedString(userHmac)
|
jstr, err := j.SignedString(UserHmac)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// we should ALWAYS be able to build and sign a str
|
// we should ALWAYS be able to build and sign a str
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
15
util/util.go
15
util/util.go
@@ -1,6 +1,10 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import "crypto/rand"
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
func GenerateHmac() []byte {
|
func GenerateHmac() []byte {
|
||||||
b := make([]byte, 64)
|
b := make([]byte, 64)
|
||||||
@@ -10,3 +14,12 @@ func GenerateHmac() []byte {
|
|||||||
|
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PrincipalInfo struct {
|
||||||
|
Uid uuid.UUID
|
||||||
|
Role string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FailMsg struct {
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user