I built a backend with Golang's Gin framework and the JWT middleware for it. This is the official example from the readme, which I used:
main.go
package main
import (
"log"
"net/http"
"os"
"time"
"github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)
type login struct {
Username string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
var identityKey = "id"
func helloHandler(c *gin.Context) {
claims := jwt.ExtractClaims(c)
user, _ := c.Get(identityKey)
c.JSON(200, gin.H{
"userID": claims[identityKey],
"userName": user.(*User).UserName,
"text": "Hello World.",
})
}
// User demo
type User struct {
UserName string
FirstName string
LastName string
}
func main() {
port := os.Getenv("PORT")
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
if port == "" {
port = "8000"
}
// the jwt middleware
authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
Realm: "test zone",
Key: []byte("secret key"),
Timeout: time.Hour,
MaxRefresh: time.Hour,
IdentityKey: identityKey,
PayloadFunc: func(data interface{}) jwt.MapClaims {
if v, ok := data.(*User); ok {
return jwt.MapClaims{
identityKey: v.UserName,
}
}
return jwt.MapClaims{}
},
IdentityHandler: func(c *gin.Context) interface{} {
claims := jwt.ExtractClaims(c)
return &User{
UserName: claims[identityKey].(string),
}
},
Authenticator: func(c *gin.Context) (interface{}, error) {
var loginVals login
if err := c.ShouldBind(&loginVals); err != nil {
return "", jwt.ErrMissingLoginValues
}
userID := loginVals.Username
password := loginVals.Password
if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
return &User{
UserName: userID,
LastName: "Bo-Yi",
FirstName: "Wu",
}, nil
}
return nil, jwt.ErrFailedAuthentication
},
Authorizator: func(data interface{}, c *gin.Context) bool {
if v, ok := data.(*User); ok && v.UserName == "admin" {
return true
}
return false
},
Unauthorized: func(c *gin.Context, code int, message string) {
c.JSON(code, gin.H{
"code": code,
"message": message,
})
},
// TokenLookup is a string in the form of "<source>:<name>" that is used
// to extract token from the request.
// Optional. Default value "header:Authorization".
// Possible values:
// - "header:<name>"
// - "query:<name>"
// - "cookie:<name>"
// - "param:<name>"
TokenLookup: "header: Authorization, query: token, cookie: jwt",
// TokenLookup: "query:token",
// TokenLookup: "cookie:token",
// TokenHeadName is a string in the header. Default value is "Bearer"
TokenHeadName: "Bearer",
// TimeFunc provides the current time. You can override it to use another time value. This is useful for testing or if your server uses a different time zone than your tokens.
TimeFunc: time.Now,
})
if err != nil {
log.Fatal("JWT Error:" + err.Error())
}
r.POST("/login", authMiddleware.LoginHandler)
r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) {
claims := jwt.ExtractClaims(c)
log.Printf("NoRoute claims: %#v\n", claims)
c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
})
auth := r.Group("/auth")
// Refresh time can be longer than token timeout
auth.GET("/refresh_token", authMiddleware.RefreshHandler)
auth.Use(authMiddleware.MiddlewareFunc())
{
auth.GET("/hello", helloHandler)
}
if err := http.ListenAndServe(":"+port, r); err != nil {
log.Fatal(err)
}
}
My auth service in Angular 8 looks like this:
auth.service
headers = new HttpHeaders({ "Content-Type": "application/json" });
login(username: string, password: string): Promise<any> {
const url: string = `${this.BASE_URL}` + "/login";
const request = this.http
.post(
url,
JSON.stringify({ username: username, password: password }),
{ headers: this.headers }
)
.toPromise();
return request;
}
But this gives me an error message in Chrome:
Access to XMLHttpRequest at 'http://127.0.0.1:8000/api/login' from origin 'http://localhost:4200' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
In the console Gin returns status code 204 though.
I thought this was a CORS issue, so I implemented Gin's CORS middleware:
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:8000"},
AllowMethods: []string{"PUT", "PATCH"},
AllowHeaders: []string{"Origin"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
AllowOriginFunc: func(origin string) bool {
return origin == "https://github.com"
},
MaxAge: 12 * time.Hour,
}))
Unfortunately it still didn't work. Also not if I added the POST method to the allowed methods:
AllowMethods: []string{"PUT", "PATCH", "POST"}
My last try was to use proxy, as described in the Angular documentation:
proxy.conf.json
{
"/api": {
"target": "http://localhost:8000",
"secure": false
}
}
angular.json
"options": {
"browserTarget": "your-application-name:build",
"proxyConfig": "src/proxy.conf.json"
}
I re-started with ng serve, but the error still appears (I changed the urls to /api/login in the auth.service and main.go according to the configuration in the proxy file).
What am I doing wrong here?
Change AllowHeaders: []string{"Origin"} to AllowHeaders: []string{"content-type"};
Related
In DynamoDB I Have a table that contains:
- email (primary key)
- password (attribute)
- rname (attribute)
I'm using V1 of the AWS Go SDK, to implement to perform a query using just the primary key to my database:
My struct to unMarshal to is:
type Item struct {
Email string `json:"email"`
Password string `json:"password"`
Rname string `json:"rname"`
}
and the code:
result, err := client.Query(&dynamodb.QueryInput{
TableName: aws.String("accountsTable"),
KeyConditions: map[string]*dynamodb.Condition{
"email": {
ComparisonOperator: aws.String("EQ"),
AttributeValueList: []*dynamodb.AttributeValue{
{
S: aws.String(email),
},
},
},
},
})
if err != nil {
return false, err
}
item := []Item{}
err = dynamodbattribute.UnmarshalListOfMaps(result.Items, &item)
if err != nil {
return false, err
}
However, I get the issue that the key is invalid. I check the key in the database and it matches the one i print out to the console too.
Not sure how to get round this issue as example's i've seen seem to work for their's and look the same.
Any help in fixing this issue would be appreciated thanks :)
You need to set the values of password and rname to omitempty so that it's not set to empty values as they are not keys they should not be included on a Query as it throws an invalid key exception:
type Item struct {
Email string `json:"email" dynamodbav:"email,omitempty"`
Password string `json:"password" dynamodbav:"password,omitempty"`
Rname string `json:"rname" dynamodbav:"rname,omitempty"`
}
Update
I believe the issue is due to the fact you try to marshall the entire response in a single command, however, iterating works for me. (I do not use Go).
package main
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
"fmt"
)
func main() {
// Create Session
sess := session.Must(session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
}))
// Create DynamoDB Client with Logging
client := dynamodb.New(sess, aws.NewConfig())
type Item struct {
Email string `dynamodbav: "email"`
Password string `dynamodbav: "password,omitempty"`
Rname string `dynamodbav: "rname,omitempty"`
}
result, err := client.Query(&dynamodb.QueryInput{
TableName: aws.String("accountsTable"),
KeyConditions: map[string]*dynamodb.Condition{
"email": {
ComparisonOperator: aws.String("EQ"),
AttributeValueList: []*dynamodb.AttributeValue{
{
S: aws.String("lhnng#amazon.com"),
},
},
},
},
})
if err != nil {
fmt.Println("Query API call failed:")
fmt.Println((err.Error()))
}
for _, i := range result.Items {
item := Item{}
err = dynamodbattribute.UnmarshalMap(i, &item)
if err != nil {
fmt.Println("Got error unmarshalling: %s", err)
}
fmt.Println("Email: ", item.Email)
fmt.Println()
}
}
Moreover, as you use a single key of email, it means there is at most 1 item with the same email address, meaning you should use GetItem rather than Query:
package main
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)
func main() {
// Item to Get
type Item struct {
Email string `dynamodbav: "email"`
Password string `dynamodbav: "password,omitempty"`
Rname string `dynamodbav: "rname,omitempty"`
}
// Create Session
sess := session.Must(session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
}))
// Create DynamoDB Client
client := dynamodb.New(sess, aws.NewConfig())
// Get Item
result, err := client.GetItem(&dynamodb.GetItemInput{
TableName: aws.String("accountsTable"),
Key: map[string]*dynamodb.AttributeValue{
"email": {
S: aws.String("lhnng#amazon.com"),
},
},
})
// Catch Error
if err != nil {
fmt.Println("GetItem API call failed:")
fmt.Println((err.Error()))
}
item := Item{}
// Unmarhsall
err = dynamodbattribute.UnmarshalMap(result.Item, &item)
if err != nil {
panic(fmt.Sprintf("Failed to unmarshal Record, %v", err))
}
// If Item Returns Empty
if item.Email == "" {
fmt.Println("Could not find Item")
return
}
// Print Result
fmt.Println("Found item:")
fmt.Println("Email: ", item.Email)
}
Description
I have an API which I have created and I have some endpoints protected. The problem I am facing now on the client making a request is that the first request comes through with the Authorization header provided but a second request is blocked because Authorization is not present.
I can confirm that Authorization is present and worked perfectly when I was running Typescript till I recreated the endpoints in Go with Gin.
How to reproduce
Call estimate endpoint from client (iOS app) response succeceds
Make a second call from Client (iOS app) response failed because it is not taking the Authorization header which contains token
package main
import (
"github.com/gin-gonic/gin"
)
type App struct {
Router *gin.Engine
Gateman *gateman.Gateman
Database *mongo.Client
}
func (a *App) StartApp() {
err := godotenv.Load()
if err != nil {
fmt.Printf("Could not load .env \n")
}
a.Database = database.DB
a.Router = gin.New()
a.Gateman = middleware.Gateman()
a.Router.Use(gin.Recovery())
a.Router.Use(middleware.DefaultHelmet())
a.Router.Use(middleware.GinContextToContextMiddleware())
a.Router.Use(middleware.RequestID(nil))
a.Router.Use(middleware.ErrorHandler())
a.Router.Use(middleware.Logger("package-service"))
connection, err := amqp091.Dial(os.Getenv("AMQP_URL"))
if err != nil {
log.Fatal(fmt.Printf("Error on dial %v\n", err.Error()))
}
routes.Routes(a.Router, database.GetDatabase(a.Database), a.Gateman, connection)
}
func (a *App) Run(addr string) {
logs := log.New(os.Stdout, "package-service", log.LstdFlags)
server := &http.Server{
Addr: addr,
Handler: a.Router,
ErrorLog: logs,
IdleTimeout: 120 * time.Second, // max time for connections using TCP Keep-Alive
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
go func() {
if err := server.ListenAndServe(); err != nil {
logs.Fatal(err)
}
}()
// trap sigterm or interrupt and gracefully shutdown the server
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt)
signal.Notify(c, os.Kill)
sig := <-c
logs.Println("Recieved terminate, graceful shutdown", sig)
tc, _ := context.WithTimeout(context.Background(), 30*time.Second)
server.Shutdown(tc)
}
func Routes(r *gin.Engine, db *mongo.Database, g *gateman.Gateman, conn *amqp091.Connection) {
atHandler := pc.NewPackagesController(ps.NewPackagesService(pr.NewPackagesRepository(db)), g, events.NewEventEmitter(conn))
r.Use(CORS())
v1 := r.Group("/api/v1/package")
{
v1.POST("/query", GraphqlHandler(db, directives.NewDirectivesManager(g)))
v1.GET("/", PlaygroundHandler(db))
v1.POST("/", g.Guard([]string{"user"}, nil), atHandler.Create)
v1.POST("/estimate", g.Guard([]string{"user"}, nil), atHandler.Estimate)
v1.PUT("/:packageID", g.Guard([]string{"user", "admin"}, nil), atHandler.Update)
v1.PUT("/:packageID/assign", g.Guard([]string{"admin"}, nil), atHandler.Assign)
v1.POST("/:packageID/cancel", g.Guard([]string{"user", "admin"}, nil), atHandler.CancelRequest)
v1.POST("/:packageID/complete", g.Guard([]string{"admin"}, nil), atHandler.Complete)
v1.POST("/:packageID/reject", g.Guard([]string{"admin"}, nil), atHandler.RejectRequest)
v1.GET("/healthz", atHandler.GetHealth)
}
r.GET("/", atHandler.GetUP)
}
func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
func main() {
start := App{}
start.StartApp()
start.Run(":3009")
}
Expectations
All endpoints that are Guarded simply checks the header for Authorization and if provided in the request, it should be successful
Actual result
First request succeed /estimate
Second request / POST request fails to accept Authorization header
Also irrespective of what the first post request is, the second post request just never accept the Authorization header
Also need to mention that I do not have this issue with postman. Both request run independently but using another client for the request, gives this problem
Environment
go version: 1.19
gin version (or commit ref): v1.8.1
operating system: Mac and iOS mobile
Here is my client code
func request<T>(with builder: BaseRequest) -> AnyPublisher<T, APIError> where T: Codable {
request(with: builder, customDecoder: JSONDecoder())
}
func request<T>(with builder: BaseRequest, customDecoder: JSONDecoder) -> AnyPublisher<T, APIError> where T: Codable {
let encoding: ParametersEncoder = [.get, .delete].contains(builder.method) ? URLParameretersEncoder() : JSONParametersEncoder()
customDecoder.keyDecodingStrategy = .convertFromSnakeCase
var url: URL {
var components = URLComponents()
components.scheme = "https"
components.host = builder.baseUrl
components.path = "/api/v1" + builder.path
guard let url = components.url else {
preconditionFailure("Invalid URL components: \(components)")
}
return url
}
var urlRequest = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 46.0)
urlRequest.httpMethod = builder.method.rawValue
builder.headers.forEach { key, value in
urlRequest.setValue(value, forHTTPHeaderField: key)
}
if let token = tokenManager.token {
urlRequest.setValue("Bearer " + token, forHTTPHeaderField: "Authorization")
// urlRequest.setValue("ABC", forHTTPHeaderField: "testing123")
}
if let parameters = builder.parameters {
guard let encoded = try? encoding.encode(parameters: parameters, in: urlRequest) else {
fatalError()
}
urlRequest = encoded
}
self.log(request: urlRequest)
return URLSession.shared
.dataTaskPublisher(for: urlRequest)
.receive(on: DispatchQueue.main)
.mapError {_ -> APIError in
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
return .unknown
}
.flatMap { data, response -> AnyPublisher<T, APIError> in
guard let response = response as? HTTPURLResponse else {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
return Fail(error: APIError.invalidResponse).eraseToAnyPublisher()
}
self.log(response: response, data: data, error: nil)
if (200...299).contains(response.statusCode) {
return Just(data)
.decode(type: T.self, decoder: customDecoder)
// .map {
// print($0)
// return $0
// } //added
.mapError {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
return .decodingError(underlyingError: $0)
}
.eraseToAnyPublisher()
} else {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
if response.statusCode == 401 {
// Send notification to remove corrdinator and return to root
// rxNotificationCenter.post(.invalidToken)
}
guard let errorResponse = try? customDecoder.decode(BaseResponse.self, from: data) else {
return Fail(error: APIError.decodingError(underlyingError: NSError("Can't decode error"))).eraseToAnyPublisher()
}
return Fail(error: APIError.server(response: errorResponse))
.eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
protocol BaseRequest {
var baseUrl: String { get }
var path: String { get }
var headers: HTTPHeaders { get }
var method: HTTPRequestMethod { get }
var parameters: HTTPParameters { get }
}
public typealias HTTPHeaders = [String: String]
public typealias HTTPParameters = [String: Any]?
Another point, calling a single endpoint multiple time, works fine, calling a different one is where the header is rejected
I'm trying to send mail using aws sdk v2 for Go.
I'm getting below error. When using s3 client everything is working fine. I checked the permissions associated with the credentials and it has administrator access. Unable to understand what could be the problem.
operation error SES: SendEmail, failed to sign request: failed to retrieve credentials: request canceled, context canceled
config.go
AWSConfig, err = awsConfig.LoadDefaultConfig(context.TODO())
if err != nil {
log.Println("Error configuring aws: ", err)
}
mailer.go
type Mail struct {
To []string
From string
Subject string
Body string
}
func (m *Mail) Send(ctx context.Context) error {
sesClient := ses.NewFromConfig(config.AWSConfig)
result, err := sesClient.SendEmail(ctx, &ses.SendEmailInput{
Destination: &types.Destination{
ToAddresses: m.To,
},
Message: &types.Message{
Body: &types.Body{
Html: &types.Content{
Data: &m.Body,
Charset: &CharSet,
},
},
Subject: &types.Content{
Data: &m.Subject,
Charset: &CharSet,
},
},
Source: &m.From,
ReplyToAddresses: ReplyTo,
ReturnPath: &BounceEmail,
})
if err != nil {
log.Println(fmt.Errorf("[MailSenderUtil]: error sending mail: %w", err))
return err
}
log.Println("[MailSenderUtilResult]: ", InJson(result))
return nil
}
I think you need to add your aws credentials to config.AWSConfig. This is my config
sess, err := session.NewSession(&aws.Config{
Credentials: credentials.NewStaticCredentials(constants.AWSAccessID, constants.AWSAccessKey , ""),
Region:aws.String("eu-west-2")},
)
svc := ses.New(sess)
input := &ses.SendEmailInput{
Destination: &ses.Destination{
ToAddresses: []*string{
aws.String(Recipient),
},
},
Message: &ses.Message{
Body: &ses.Body{
Html: &ses.Content{
Charset: aws.String(CharSet),
Data: aws.String(HtmlBody),
},
Text: &ses.Content{
Charset: aws.String(CharSet),
Data: aws.String(TextBody),
},
},
Subject: &ses.Content{
Charset: aws.String(CharSet),
Data: aws.String(Subject),
},
},
Source: aws.String(Sender),
// Uncomment to use a configuration set
//ConfigurationSetName: aws.String(ConfigurationSet),
}
// Attempt to send the email.
_, err = svc.SendEmail(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case ses.ErrCodeMessageRejected:
logger.Logger.Error(ses.ErrCodeMessageRejected, aerr.OrigErr())
case ses.ErrCodeMailFromDomainNotVerifiedException:
logger.Logger.Error(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.OrigErr())
case ses.ErrCodeConfigurationSetDoesNotExistException:
logger.Logger.Error(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.OrigErr())
default:
logger.Logger.Error("amazon default error", aerr.OrigErr())
}
} else {
logger.Logger.Error("amazon default error (else)", aerr.OrigErr())
}
return
}
I'm working with framework GIN and Gin-JWT in Golang.
So far so good, I was able to authorize and authenticate my REST API with JWT following the example in Gin-JWT package.
I'm trying now to implement some kind of Role in my API.
The flow would be:
Login and auth
Create the JWT with inside the userID and the RoleID
When I call a REST API I confront the role associated to the API with the RoleID in JWT to authorized
So far I have this in my main:
jwtAfp := InitJwtMiddleware(db)
afb := r.Group("api/v1/afb")
afb.Use(jwtAfp.MiddlewareFunc())
afb.GET("/ping", afbController.Ping)
and this for the InitJwtMiddleware using Gin-JWT
func InitJwtMiddleware(db *gorm.DB) *jwt.GinJWTMiddleware {
return &jwt.GinJWTMiddleware{
Realm: "afb",
Key: []byte("secret pwd"),
Timeout: time.Hour,
MaxRefresh: time.Hour,
PayloadFunc: func(data interface{}) jwt.MapClaims {
if v, ok := data.(*model.User); ok {
return jwt.MapClaims{
"afb": v.ID,
}
}
return jwt.MapClaims{}
},
Authenticator: func(c *gin.Context) (interface{}, error) {
var loginVals login
if err := c.Bind(&loginVals); err != nil {
return "", jwt.ErrMissingLoginValues
}
email := loginVals.Username
password := loginVals.Password
var u model.User
db.Where("email = ?", email).First(&u)
if service.CheckPasswordHash(password, u.Password) {
return &u, nil
}
return nil, jwt.ErrFailedAuthentication
},
Authorizator: func(data interface{}, c *gin.Context) bool {
claims := jwt.ExtractClaims(c)
v, ok := data.(float64)
if ok && v == claims["afb"] {
return true
}
return false
},
Unauthorized: func(c *gin.Context, code int, message string) {
c.JSON(code, gin.H{
"code": code,
"message": message,
})
},
TokenHeadName: "Bearer",
TimeFunc: time.Now,
}
}
I would like to add the checking on the Role in the Authorizator section but I'm struggling on how i can do this.
I come up with passing in the InitJwtMiddleware(db) function also the role, this will work but I don't like the idea to "instaziate" a GinJWTMiddleware for each ROLE/API. Or if I could know inside the middleware which function (controller) will be called later I can then figure out if authorize or not. But even this solutin sound awkward to me. I think there will be a most elegant solution, any ideas?
You can try this:
https://github.com/kyfk/gin-jwt
It's the simplest auth[orization/entication] library.
The VerifyPerm function could be helpful for role management.
There's a complete example
func main() {
auth, err := jwt.New(jwt.Auth{
SecretKey: []byte("must change here"),
// Authenticator authenticates a request and return jwt.MapClaims
// that contains a user information of the request.
Authenticator: func(c *gin.Context) (jwt.MapClaims, error) {
var loginForm LoginForm
if err := c.ShouldBind(&loginForm); err != nil {
return nil, jwt.ErrorAuthenticationFailed
}
u, ok := authenticate(req.Username, req.Password)
if ok {
return nil, jwt.ErrorAuthenticationFailed
}
return jwt.MapClaims{
"username": u.Username,
"role": u.Role,
}, nil
},
// UserFetcher takes a jwt.MapClaims and return a user object.
UserFetcher: func(c *gin.Context, claims jwt.MapClaims) (interface{}, error) {
username, ok := claims["username"].(string)
if !ok {
return nil, nil
}
return findByUsername(username)
},
})
// some lines
e.Use(jwt.ErrorHandler)
// issue authorization token
e.POST("/login", auth.AuthenticateHandler)
// refresh token expiration
e.POST("/auth/refresh_token", auth.RefreshHandler)
// role management
e.GET("/operator/hello", Operator(auth), SayHello) // this is only for Operator
e.GET("/admin/hello", Admin(auth), SayHello) // this is only for Admin
}
func Operator(m jwt.Auth) gin.HandlerFunc {
return m.VerifyPerm(func(claims jwt.MapClaims) bool {
return role(claims).IsOperator()
})
}
func Admin(m jwt.Auth) gin.HandlerFunc {
return m.VerifyPerm(func(claims jwt.MapClaims) bool {
return role(claims).IsAdmin()
})
}
Here is my simple rest service:
// Package classification User API.
//
// the purpose of this application is to provide an application
// that is using plain go code to define an API
//
// This should demonstrate all the possible comment annotations
// that are available to turn go code into a fully compliant swagger 2.0 spec
//
// Terms Of Service:
//
// there are no TOS at this moment, use at your own risk we take no responsibility
//
// Schemes: http, https
// Host: localhost
// BasePath: /v2
// Version: 0.0.1
// License: MIT http://opensource.org/licenses/MIT
// Contact: John Doe<john.doe#example.com> http://john.doe.com
//
// Consumes:
// - application/json
// - application/xml
//
// Produces:
// - application/json
// - application/xml
//
//
// swagger:meta
package main
import (
"github.com/gin-gonic/gin"
"strconv"
"database/sql"
_ "github.com/go-sql-driver/mysql"
"gopkg.in/gorp.v1"
"log"
)
// swagger:model
// User represents the user for this application
//
// A user is the security principal for this application.
// It's also used as one of main axis for reporting.
//
// A user can have friends with whom they can share what they like.
//
type User struct {
// the id for this user
//
// required: true
// min: 1
Id int64 `db:"id" json:"id"`
// the first name for this user
// required: true
// min length: 3
Firstname string `db:"firstname" json:"firstname"`
// the last name for this user
// required: true
// min length: 3
Lastname string `db:"lastname" json:"lastname"`
}
func main() {
r := gin.Default()
r.Use(Cors())
v1 := r.Group("api/v1")
{
v1.GET("/users", GetUsers)
v1.GET("/users/:id", GetUser)
v1.POST("/users", PostUser)
v1.PUT("/users/:id", UpdateUser)
v1.DELETE("/users/:id", DeleteUser)
v1.OPTIONS("/users", OptionsUser) // POST
v1.OPTIONS("/users/:id", OptionsUser) // PUT, DELETE
}
r.Run(":8696")
}
func GetUsers(c *gin.Context) {
// swagger:route GET /user listPets pets users
//
// Lists pets filtered by some parameters.
//
// This will show all available pets by default.
// You can get the pets that are out of stock
//
// Consumes:
// - application/json
// - application/x-protobuf
//
// Produces:
// - application/json
// - application/x-protobuf
//
// Schemes: http, https, ws, wss
//
// Security:
// api_key:
// oauth: read, write
//
// Responses:
// default: genericError
// 200: someResponse
// 422: validationError
var users []User
_, err := dbmap.Select(&users, "SELECT * FROM user")
if err == nil {
c.JSON(200, users)
} else {
c.JSON(404, gin.H{"error": "no user(s) into the table"})
}
// curl -i http://localhost:8080/api/v1/users
}
func GetUser(c *gin.Context) {
id := c.Params.ByName("id")
var user User
err := dbmap.SelectOne(&user, "SELECT * FROM user WHERE id=?", id)
if err == nil {
user_id, _ := strconv.ParseInt(id, 0, 64)
content := &User{
Id: user_id,
Firstname: user.Firstname,
Lastname: user.Lastname,
}
c.JSON(200, content)
} else {
c.JSON(404, gin.H{"error": "user not found"})
}
// curl -i http://localhost:8080/api/v1/users/1
}
func PostUser(c *gin.Context) {
var user User
c.Bind(&user)
if user.Firstname != "" && user.Lastname != "" {
if insert, _ := dbmap.Exec(`INSERT INTO user (firstname, lastname) VALUES (?, ?)`, user.Firstname, user.Lastname); insert != nil {
user_id, err := insert.LastInsertId()
if err == nil {
content := &User{
Id: user_id,
Firstname: user.Firstname,
Lastname: user.Lastname,
}
c.JSON(201, content)
} else {
checkErr(err, "Insert failed")
}
}
} else {
c.JSON(422, gin.H{"error": "fields are empty"})
}
// curl -i -X POST -H "Content-Type: application/json" -d "{ \"firstname\": \"Thea\", \"lastname\": \"Queen\" }" http://localhost:8080/api/v1/users
}
func UpdateUser(c *gin.Context) {
id := c.Params.ByName("id")
var user User
err := dbmap.SelectOne(&user, "SELECT * FROM user WHERE id=?", id)
if err == nil {
var json User
c.Bind(&json)
user_id, _ := strconv.ParseInt(id, 0, 64)
user := User{
Id: user_id,
Firstname: json.Firstname,
Lastname: json.Lastname,
}
if user.Firstname != "" && user.Lastname != ""{
_, err = dbmap.Update(&user)
if err == nil {
c.JSON(200, user)
} else {
checkErr(err, "Updated failed")
}
} else {
c.JSON(422, gin.H{"error": "fields are empty"})
}
} else {
c.JSON(404, gin.H{"error": "user not found"})
}
// curl -i -X PUT -H "Content-Type: application/json" -d "{ \"firstname\": \"Thea\", \"lastname\": \"Merlyn\" }" http://localhost:8080/api/v1/users/1
}
func DeleteUser(c *gin.Context) {
id := c.Params.ByName("id")
var user User
err := dbmap.SelectOne(&user, "SELECT id FROM user WHERE id=?", id)
if err == nil {
_, err = dbmap.Delete(&user)
if err == nil {
c.JSON(200, gin.H{"id #" + id: " deleted"})
} else {
checkErr(err, "Delete failed")
}
} else {
c.JSON(404, gin.H{"error": "user not found"})
}
// curl -i -X DELETE http://localhost:8080/api/v1/users/1
}
var dbmap = initDb()
func initDb() *gorp.DbMap {
db, err := sql.Open("mysql",
"root:max_123#tcp(127.0.0.1:3306)/gotest")
checkErr(err, "sql.Open failed")
dbmap := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}}
dbmap.AddTableWithName(User{}, "User").SetKeys(true, "Id")
err = dbmap.CreateTablesIfNotExists()
checkErr(err, "Create table failed")
return dbmap
}
func checkErr(err error, msg string) {
if err != nil {
log.Fatalln(msg, err)
}
}
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Add("Access-Control-Allow-Origin", "*")
c.Next()
}
}
func OptionsUser(c *gin.Context) {
c.Writer.Header().Add("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "DELETE,POST, PUT")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type")
c.Next()
}
Now when I'm executing :
swagger generate spec -o ./swagger.json
to generate the json spec I'm getting:
{
"consumes": ["application/json", "application/xml"],
"produces": ["application/json", "application/xml"],
"schemes": ["http", "https"],
"swagger": "2.0",
"info": {
"description": "the purpose of this application is to provide an application\nthat is using plain go code to define an API\n\nThis should demonstrate all the possible comment annotations\nthat are available to turn go code into a fully compliant swagger 2.0 spec",
"title": "User API.",
"termsOfService": "there are no TOS at this moment, use at your own risk we take no responsibility",
"contact": {
"name": "John Doe",
"url": "http://john.doe.com",
"email": "john.doe#example.com"
},
"license": {
"name": "MIT",
"url": "http://opensource.org/licenses/MIT"
},
"version": "0.0.1"
},
"host": "localhost",
"basePath": "/v2",
"paths": {
"/user": {
"get": {
"description": "This will show all available pets by default.\nYou can get the pets that are out of stock",
"consumes": ["application/json", "application/x-protobuf"],
"produces": ["application/json", "application/x-protobuf"],
"schemes": ["http", "https", "ws", "wss"],
"tags": ["listPets", "pets"],
"summary": "Lists pets filtered by some parameters.",
"operationId": "users",
"security": [{
"api_key": null
}, {
"oauth": ["read", "write"]
}],
"responses": {
"200": {
"$ref": "#/responses/someResponse"
},
"422": {
"$ref": "#/responses/validationError"
},
"default": {
"$ref": "#/responses/genericError"
}
}
}
}
},
"definitions": {}
}
Note that my definitions are empty, not sure why.
If I paste the same json spec in http://editor.swagger.io/#/
It says
Error
Object
message: "options.definition is required"
code: "UNCAUGHT_SWAY_WORKER_ERROR"
Any directions on what is the right way to generate swagger documentation would help
It's because go-swagger can't detect the usage of your definitions. I made the assumption that you'd always have a struct describing the parameters and that those would always use the definitions that are in use.
It would be great if you could submit this question as an issue on the repo with your sample program. I'll add it to my tests.