add(auth): separate webservice for auth
This commit is contained in:
parent
d7cac7dbc1
commit
bdb0844a48
36
auth/docs/src/spec.json
Normal file
36
auth/docs/src/spec.json
Normal file
@ -0,0 +1,36 @@
|
||||
[
|
||||
{
|
||||
"title": "Authentication",
|
||||
"package": "auth",
|
||||
"path": "/auth",
|
||||
"entrypoint": "auth",
|
||||
"authentication": [],
|
||||
"apis": [
|
||||
{
|
||||
"name": "login",
|
||||
"method": "POST",
|
||||
"title": "User login",
|
||||
"parameters": {
|
||||
"post": [
|
||||
{ "type": "string", "name": "username", "required": true, "title": "Username or email" },
|
||||
{ "type": "string", "name": "password", "required": true, "title": "Password for user" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "create",
|
||||
"path": "/create",
|
||||
"method": "POST",
|
||||
"title": "Create new user",
|
||||
"parameters": {
|
||||
"post": [
|
||||
{ "type": "string", "name": "name", "required": true, "title": "Display name" },
|
||||
{ "type": "string", "name": "email", "required": true, "title": "Email" },
|
||||
{ "type": "string", "name": "username", "required": true, "title": "Username" },
|
||||
{ "type": "string", "name": "password", "required": true, "title": "Password" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
68
auth/docs/src/spec/auth.json
Normal file
68
auth/docs/src/spec/auth.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"Title": "Authentication",
|
||||
"Package": "auth",
|
||||
"Interface": "Auth",
|
||||
"Struct": null,
|
||||
"Parameters": null,
|
||||
"Protocol": "",
|
||||
"Authentication": [],
|
||||
"Path": "/auth",
|
||||
"APIs": [
|
||||
{
|
||||
"Name": "login",
|
||||
"Method": "POST",
|
||||
"Title": "User login",
|
||||
"Path": "/login",
|
||||
"Parameters": {
|
||||
"post": [
|
||||
{
|
||||
"name": "username",
|
||||
"required": true,
|
||||
"title": "Username or email",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"required": true,
|
||||
"title": "Password for user",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "create",
|
||||
"Method": "POST",
|
||||
"Title": "Create new user",
|
||||
"Path": "/create",
|
||||
"Parameters": {
|
||||
"post": [
|
||||
{
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"title": "Display name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"required": true,
|
||||
"title": "Email",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"required": true,
|
||||
"title": "Username",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"required": true,
|
||||
"title": "Password",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,31 +1,52 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/namsral/flag"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
configuration struct {
|
||||
jwtSecret string
|
||||
jwtExpiry int64
|
||||
jwtDebug bool
|
||||
http *httpFlags
|
||||
db *dbFlags
|
||||
jwt *jwtFlags
|
||||
}
|
||||
)
|
||||
|
||||
var config configuration
|
||||
var config *configuration
|
||||
|
||||
func (c configuration) validate() error {
|
||||
if c.jwtSecret == "" {
|
||||
return errors.New("JWT Secret not set for AUTH")
|
||||
func (configuration) New() *configuration {
|
||||
return &configuration{
|
||||
new(httpFlags),
|
||||
new(dbFlags),
|
||||
new(jwtFlags),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *configuration) validate() error {
|
||||
if c == nil {
|
||||
return errors.New("CRM config is not initialized, need to call Flags()")
|
||||
}
|
||||
if err := c.http.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.db.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.jwt.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flags should be called from main to register flags
|
||||
func Flags(_ ...string) {
|
||||
flag.StringVar(&config.jwtSecret, "auth-jwt-secret", "", "JWT Secret")
|
||||
flag.Int64Var(&config.jwtExpiry, "auth-jwt-expiry", 3600, "JWT Expiration in minutes")
|
||||
flag.BoolVar(&config.jwtDebug, "auth-jwt-debug", false, "Generate debug JWT key")
|
||||
func Flags(prefix ...string) {
|
||||
if config != nil {
|
||||
return
|
||||
}
|
||||
if len(prefix) == 0 {
|
||||
panic("crm.Flags() needs prefix on first call")
|
||||
}
|
||||
config = configuration{}.New()
|
||||
config.http.flags(prefix...)
|
||||
config.db.flags(prefix...)
|
||||
config.jwt.flags(prefix...)
|
||||
}
|
||||
|
||||
29
auth/flags_db.go
Normal file
29
auth/flags_db.go
Normal file
@ -0,0 +1,29 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/namsral/flag"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
dbFlags struct {
|
||||
dsn string
|
||||
profiler string
|
||||
}
|
||||
)
|
||||
|
||||
func (c *dbFlags) validate() error {
|
||||
if c.dsn == "" {
|
||||
return errors.New("No DB DSN is set, can't connect to database")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *dbFlags) flags(prefix ...string) {
|
||||
p := func(s string) string {
|
||||
return prefix[0] + "-" + s
|
||||
}
|
||||
|
||||
flag.StringVar(&c.dsn, p("db-dsn"), "crust:crust@tcp(db1:3306)/crust?collation=utf8mb4_general_ci", "DSN for database connection")
|
||||
flag.StringVar(&c.profiler, p("db-profiler"), "", "Profiler for DB queries (none, stdout)")
|
||||
}
|
||||
35
auth/flags_http.go
Normal file
35
auth/flags_http.go
Normal file
@ -0,0 +1,35 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/namsral/flag"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
httpFlags struct {
|
||||
addr string
|
||||
logging bool
|
||||
pretty bool
|
||||
tracing bool
|
||||
metrics bool
|
||||
}
|
||||
)
|
||||
|
||||
func (c *httpFlags) validate() error {
|
||||
if c.addr == "" {
|
||||
return errors.New("No HTTP Addr is set, can't listen for HTTP")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *httpFlags) flags(prefix ...string) {
|
||||
p := func(s string) string {
|
||||
return prefix[0] + "-" + s
|
||||
}
|
||||
|
||||
flag.StringVar(&c.addr, p("http-addr"), ":3000", "Listen address for HTTP server")
|
||||
flag.BoolVar(&c.logging, p("http-log"), true, "Enable/disable HTTP request log")
|
||||
flag.BoolVar(&c.pretty, p("http-pretty-json"), false, "Prettify returned JSON output")
|
||||
flag.BoolVar(&c.tracing, p("http-error-tracing"), false, "Return error stack frame")
|
||||
flag.BoolVar(&c.metrics, p("http-metrics"), false, "Provide metrics export for prometheus")
|
||||
}
|
||||
27
auth/flags_jwt.go
Normal file
27
auth/flags_jwt.go
Normal file
@ -0,0 +1,27 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/namsral/flag"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
jwtFlags struct {
|
||||
secret string
|
||||
expiry int64
|
||||
debugToken bool
|
||||
}
|
||||
)
|
||||
|
||||
func (c *jwtFlags) validate() error {
|
||||
if c.secret == "" {
|
||||
return errors.New("JWT Secret not set for AUTH")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *jwtFlags) flags(prefix ...string) {
|
||||
flag.StringVar(&c.secret, "auth-jwt-secret", "", "JWT Secret")
|
||||
flag.Int64Var(&c.expiry, "auth-jwt-expiry", 3600, "JWT Expiration in minutes")
|
||||
flag.BoolVar(&c.debugToken, "auth-jwt-debug", false, "Generate debug JWT key")
|
||||
}
|
||||
@ -2,9 +2,11 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/crusttech/crust/auth/types"
|
||||
"github.com/go-chi/jwtauth"
|
||||
"github.com/pkg/errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -34,12 +36,12 @@ func getIdentityClaimFromContext(ctx context.Context) (uint64, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func SetIdentityToContext(ctx context.Context, identity Identifiable) context.Context {
|
||||
func SetIdentityToContext(ctx context.Context, identity types.Identifiable) context.Context {
|
||||
return context.WithValue(ctx, identityCtxKey, identity)
|
||||
}
|
||||
|
||||
func GetIdentityFromContext(ctx context.Context) Identifiable {
|
||||
if identity, ok := ctx.Value(identityCtxKey).(Identifiable); ok {
|
||||
func GetIdentityFromContext(ctx context.Context) types.Identifiable {
|
||||
if identity, ok := ctx.Value(identityCtxKey).(types.Identifiable); ok {
|
||||
return identity
|
||||
} else {
|
||||
return NewIdentity(0)
|
||||
14
auth/jwt.go
14
auth/jwt.go
@ -1,12 +1,14 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/go-chi/jwtauth"
|
||||
"github.com/titpetric/factory/resputil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/crusttech/crust/auth/types"
|
||||
"github.com/go-chi/jwtauth"
|
||||
"github.com/titpetric/factory/resputil"
|
||||
)
|
||||
|
||||
type jwt struct {
|
||||
@ -18,9 +20,9 @@ func JWT() (*jwt, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jwt := &jwt{tokenAuth: jwtauth.New("HS256", []byte(config.jwtSecret), nil)}
|
||||
jwt := &jwt{tokenAuth: jwtauth.New("HS256", []byte(config.jwt.secret), nil)}
|
||||
|
||||
if config.jwtDebug {
|
||||
if config.jwt.debugToken {
|
||||
log.Println("DEBUG JWT TOKEN:", jwt.Encode(NewIdentity(1)))
|
||||
}
|
||||
|
||||
@ -32,11 +34,11 @@ func (t *jwt) Verifier() func(http.Handler) http.Handler {
|
||||
return jwtauth.Verifier(t.tokenAuth)
|
||||
}
|
||||
|
||||
func (t *jwt) Encode(identity Identifiable) string {
|
||||
func (t *jwt) Encode(identity types.Identifiable) string {
|
||||
// @todo Set expiry
|
||||
claims := jwtauth.Claims{}
|
||||
claims.Set("sub", strconv.FormatUint(identity.Identity(), 10))
|
||||
claims.SetExpiryIn(time.Duration(config.jwtExpiry) * time.Minute)
|
||||
claims.SetExpiryIn(time.Duration(config.jwt.expiry) * time.Minute)
|
||||
|
||||
_, jwt, _ := t.tokenAuth.Encode(claims)
|
||||
return jwt
|
||||
|
||||
20
auth/metrics.go
Normal file
20
auth/metrics.go
Normal file
@ -0,0 +1,20 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/766b/chi-prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type metrics struct{}
|
||||
|
||||
// Middleware is the request logger that provides metrics to prometheus
|
||||
func (metrics) Middleware(name string) func(http.Handler) http.Handler {
|
||||
return chiprometheus.NewMiddleware(name)
|
||||
}
|
||||
|
||||
// Handler exports prometheus metrics for /metrics requests
|
||||
func (metrics) Handler() http.Handler {
|
||||
return prometheus.Handler()
|
||||
}
|
||||
19
auth/repository/db.go
Normal file
19
auth/repository/db.go
Normal file
@ -0,0 +1,19 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/titpetric/factory"
|
||||
)
|
||||
|
||||
var _db *factory.DB
|
||||
|
||||
func DB(ctxs ...context.Context) *factory.DB {
|
||||
if _db == nil {
|
||||
_db = factory.Database.MustGet()
|
||||
}
|
||||
for _, ctx := range ctxs {
|
||||
_db = _db.With(ctx)
|
||||
break
|
||||
}
|
||||
return _db
|
||||
}
|
||||
18
auth/repository/error.go
Normal file
18
auth/repository/error.go
Normal file
@ -0,0 +1,18 @@
|
||||
package repository
|
||||
|
||||
type (
|
||||
repositoryError string
|
||||
)
|
||||
|
||||
const (
|
||||
ErrDatabaseError = repositoryError("DatabaseError")
|
||||
ErrNotImplemented = repositoryError("NotImplemented")
|
||||
)
|
||||
|
||||
func (e repositoryError) Error() string {
|
||||
return e.String()
|
||||
}
|
||||
|
||||
func (e repositoryError) String() string {
|
||||
return "crust.auth.repository." + string(e)
|
||||
}
|
||||
44
auth/repository/repository.go
Normal file
44
auth/repository/repository.go
Normal file
@ -0,0 +1,44 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/titpetric/factory"
|
||||
)
|
||||
|
||||
type (
|
||||
repository struct {
|
||||
ctx context.Context
|
||||
|
||||
// Get database handle
|
||||
dbh func(ctxs ...context.Context) *factory.DB
|
||||
}
|
||||
|
||||
Repository interface {
|
||||
Context() context.Context
|
||||
DB() *factory.DB
|
||||
}
|
||||
)
|
||||
|
||||
// With updates repository and database contexts
|
||||
func (r *repository) With(ctx context.Context) *repository {
|
||||
res := &repository{
|
||||
ctx: ctx,
|
||||
dbh: DB,
|
||||
}
|
||||
if r != nil {
|
||||
res.dbh = r.dbh
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (r *repository) Context() context.Context {
|
||||
return r.ctx
|
||||
}
|
||||
|
||||
// Return context-aware db handle
|
||||
func (r *repository) db() *factory.DB {
|
||||
return r.dbh(r.ctx)
|
||||
}
|
||||
func (r *repository) DB() *factory.DB {
|
||||
return r.db()
|
||||
}
|
||||
108
auth/repository/user.go
Normal file
108
auth/repository/user.go
Normal file
@ -0,0 +1,108 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/crusttech/crust/auth/types"
|
||||
"github.com/titpetric/factory"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
user struct {
|
||||
*repository
|
||||
}
|
||||
|
||||
User interface {
|
||||
Repository
|
||||
|
||||
With(context.Context) User
|
||||
|
||||
FindUserByUsername(username string) (*types.User, error)
|
||||
FindUserByID(id uint64) (*types.User, error)
|
||||
FindUsers(filter *types.UserFilter) ([]*types.User, error)
|
||||
CreateUser(mod *types.User) (*types.User, error)
|
||||
UpdateUser(mod *types.User) (*types.User, error)
|
||||
SuspendUserByID(id uint64) error
|
||||
UnsuspendUserByID(id uint64) error
|
||||
DeleteUserByID(id uint64) error
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
sqlUserScope = "suspended_at IS NULL AND deleted_at IS NULL"
|
||||
sqlUserSelect = "SELECT * FROM users WHERE " + sqlUserScope
|
||||
|
||||
ErrUserNotFound = repositoryError("UserNotFound")
|
||||
)
|
||||
|
||||
func NewUser(ctx context.Context) User {
|
||||
return (&user{}).With(ctx)
|
||||
}
|
||||
|
||||
func (r *user) With(ctx context.Context) User {
|
||||
return &user{
|
||||
repository: r.repository.With(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *user) FindUserByUsername(username string) (*types.User, error) {
|
||||
sql := "SELECT * FROM users WHERE username = ? AND " + sqlUserScope
|
||||
mod := &types.User{}
|
||||
|
||||
return mod, isFound(r.db().Get(mod, sql, username), mod.ID > 0, ErrUserNotFound)
|
||||
}
|
||||
|
||||
func (r *user) FindUserByID(id uint64) (*types.User, error) {
|
||||
sql := "SELECT * FROM users WHERE id = ? AND " + sqlUserScope
|
||||
mod := &types.User{}
|
||||
|
||||
return mod, isFound(r.db().Get(mod, sql, id), mod.ID > 0, ErrUserNotFound)
|
||||
}
|
||||
|
||||
func (r *user) FindUsers(filter *types.UserFilter) ([]*types.User, error) {
|
||||
rval := make([]*types.User, 0)
|
||||
params := make([]interface{}, 0)
|
||||
sql := "SELECT * FROM users WHERE " + sqlUserScope
|
||||
|
||||
if filter != nil {
|
||||
if filter.Query != "" {
|
||||
sql += " AND username LIKE ?"
|
||||
params = append(params, filter.Query+"%")
|
||||
}
|
||||
|
||||
if filter.MembersOfChannel > 0 {
|
||||
sql += " AND id IN (SELECT rel_user FROM channel_members WHERE rel_channel = ?)"
|
||||
params = append(params, filter.MembersOfChannel)
|
||||
}
|
||||
}
|
||||
|
||||
sql += " ORDER BY username ASC"
|
||||
|
||||
return rval, r.db().Select(&rval, sql, params...)
|
||||
}
|
||||
|
||||
func (r *user) CreateUser(mod *types.User) (*types.User, error) {
|
||||
mod.ID = factory.Sonyflake.NextID()
|
||||
mod.CreatedAt = time.Now()
|
||||
mod.Meta = coalesceJson(mod.Meta, []byte("{}"))
|
||||
return mod, r.db().Insert("users", mod)
|
||||
}
|
||||
|
||||
func (r *user) UpdateUser(mod *types.User) (*types.User, error) {
|
||||
mod.UpdatedAt = timeNowPtr()
|
||||
mod.Meta = coalesceJson(mod.Meta, []byte("{}"))
|
||||
|
||||
return mod, r.db().Replace("users", mod)
|
||||
}
|
||||
|
||||
func (r *user) SuspendUserByID(id uint64) error {
|
||||
return r.updateColumnByID("users", "suspend_at", time.Now(), id)
|
||||
}
|
||||
|
||||
func (r *user) UnsuspendUserByID(id uint64) error {
|
||||
return r.updateColumnByID("users", "suspend_at", nil, id)
|
||||
}
|
||||
|
||||
func (r *user) DeleteUserByID(id uint64) error {
|
||||
return r.updateColumnByID("users", "deleted_at", nil, id)
|
||||
}
|
||||
44
auth/repository/util.go
Normal file
44
auth/repository/util.go
Normal file
@ -0,0 +1,44 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (r repository) updateColumnByID(tableName, columnName string, value interface{}, id uint64) (err error) {
|
||||
return exec(r.db().Exec(
|
||||
fmt.Sprintf("UPDATE %s SET %s = ? WHERE id = ?", tableName, columnName),
|
||||
value,
|
||||
id))
|
||||
}
|
||||
|
||||
func exec(_ interface{}, err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Returns err if set otherwise it returns nerr if not valid
|
||||
func isFound(err error, valid bool, nerr error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !valid {
|
||||
return nerr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func timeNowPtr() *time.Time {
|
||||
n := time.Now()
|
||||
return &n
|
||||
}
|
||||
|
||||
func coalesceJson(vals ...json.RawMessage) json.RawMessage {
|
||||
for _, val := range vals {
|
||||
if val != nil {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
50
auth/rest/auth.go
Normal file
50
auth/rest/auth.go
Normal file
@ -0,0 +1,50 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/crusttech/crust/auth/rest/request"
|
||||
"github.com/crusttech/crust/auth/service"
|
||||
"github.com/crusttech/crust/auth/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var _ = errors.Wrap
|
||||
|
||||
type (
|
||||
Auth struct {
|
||||
user service.UserService
|
||||
token types.TokenEncoder
|
||||
}
|
||||
)
|
||||
|
||||
func (Auth) New(credValidator service.UserService, tknEncoder types.TokenEncoder) *Auth {
|
||||
return &Auth{
|
||||
credValidator,
|
||||
tknEncoder,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *Auth) Login(ctx context.Context, r *request.AuthLogin) (interface{}, error) {
|
||||
return ctrl.tokenize(ctrl.user.With(ctx).ValidateCredentials(r.Username, r.Password))
|
||||
}
|
||||
|
||||
func (ctrl *Auth) Create(ctx context.Context, r *request.AuthCreate) (interface{}, error) {
|
||||
user := &types.User{Username: r.Username}
|
||||
user.GeneratePassword(r.Password)
|
||||
return ctrl.tokenize(ctrl.user.With(ctx).Create(user))
|
||||
}
|
||||
|
||||
// Wraps user return value and appends JWT
|
||||
func (ctrl *Auth) tokenize(user *types.User, err error) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return struct {
|
||||
JWT string
|
||||
User *types.User `json:"user"`
|
||||
}{
|
||||
JWT: ctrl.token.Encode(user),
|
||||
User: user,
|
||||
}, nil
|
||||
}
|
||||
67
auth/rest/handlers/auth.go
Normal file
67
auth/rest/handlers/auth.go
Normal file
@ -0,0 +1,67 @@
|
||||
package handlers
|
||||
|
||||
/*
|
||||
Hello! This file is auto-generated from `docs/src/spec.json`.
|
||||
|
||||
For development:
|
||||
In order to update the generated files, edit this file under the location,
|
||||
add your struct fields, imports, API definitions and whatever you want, and:
|
||||
|
||||
1. run [spec](https://github.com/titpetric/spec) in the same folder,
|
||||
2. run `./_gen.php` in this folder.
|
||||
|
||||
You may edit `auth.go`, `auth.util.go` or `auth_test.go` to
|
||||
implement your API calls, helper functions and tests. The file `auth.go`
|
||||
is only generated the first time, and will not be overwritten if it exists.
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-chi/chi"
|
||||
"net/http"
|
||||
|
||||
"github.com/titpetric/factory/resputil"
|
||||
|
||||
"github.com/crusttech/crust/auth/rest/request"
|
||||
)
|
||||
|
||||
// Internal API interface
|
||||
type AuthAPI interface {
|
||||
Login(context.Context, *request.AuthLogin) (interface{}, error)
|
||||
Create(context.Context, *request.AuthCreate) (interface{}, error)
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
type Auth struct {
|
||||
Login func(http.ResponseWriter, *http.Request)
|
||||
Create func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func NewAuth(ah AuthAPI) *Auth {
|
||||
return &Auth{
|
||||
Login: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAuthLogin()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return ah.Login(r.Context(), params)
|
||||
})
|
||||
},
|
||||
Create: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAuthCreate()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return ah.Create(r.Context(), params)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (ah *Auth) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middlewares...)
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Post("/login", ah.Login)
|
||||
r.Post("/create", ah.Create)
|
||||
})
|
||||
})
|
||||
}
|
||||
139
auth/rest/request/auth.go
Normal file
139
auth/rest/request/auth.go
Normal file
@ -0,0 +1,139 @@
|
||||
package request
|
||||
|
||||
/*
|
||||
Hello! This file is auto-generated from `docs/src/spec.json`.
|
||||
|
||||
For development:
|
||||
In order to update the generated files, edit this file under the location,
|
||||
add your struct fields, imports, API definitions and whatever you want, and:
|
||||
|
||||
1. run [spec](https://github.com/titpetric/spec) in the same folder,
|
||||
2. run `./_gen.php` in this folder.
|
||||
|
||||
You may edit `auth.go`, `auth.util.go` or `auth_test.go` to
|
||||
implement your API calls, helper functions and tests. The file `auth.go`
|
||||
is only generated the first time, and will not be overwritten if it exists.
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/jmoiron/sqlx/types"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = types.JSONText{}
|
||||
|
||||
// Auth login request parameters
|
||||
type AuthLogin struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func NewAuthLogin() *AuthLogin {
|
||||
return &AuthLogin{}
|
||||
}
|
||||
|
||||
func (a *AuthLogin) Fill(r *http.Request) error {
|
||||
var err error
|
||||
|
||||
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(r.Body).Decode(a)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
r.ParseForm()
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := r.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := r.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
if val, ok := post["username"]; ok {
|
||||
|
||||
a.Username = val
|
||||
}
|
||||
if val, ok := post["password"]; ok {
|
||||
|
||||
a.Password = val
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewAuthLogin()
|
||||
|
||||
// Auth create request parameters
|
||||
type AuthCreate struct {
|
||||
Name string
|
||||
Email string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func NewAuthCreate() *AuthCreate {
|
||||
return &AuthCreate{}
|
||||
}
|
||||
|
||||
func (a *AuthCreate) Fill(r *http.Request) error {
|
||||
var err error
|
||||
|
||||
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(r.Body).Decode(a)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
r.ParseForm()
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := r.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := r.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
if val, ok := post["name"]; ok {
|
||||
|
||||
a.Name = val
|
||||
}
|
||||
if val, ok := post["email"]; ok {
|
||||
|
||||
a.Email = val
|
||||
}
|
||||
if val, ok := post["username"]; ok {
|
||||
|
||||
a.Username = val
|
||||
}
|
||||
if val, ok := post["password"]; ok {
|
||||
|
||||
a.Password = val
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewAuthCreate()
|
||||
10
auth/rest/request/misc.go
Normal file
10
auth/rest/request/misc.go
Normal file
@ -0,0 +1,10 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// RequestFiller is an interface for typed request parameters
|
||||
type RequestFiller interface {
|
||||
Fill(r *http.Request) error
|
||||
}
|
||||
17
auth/rest/router.go
Normal file
17
auth/rest/router.go
Normal file
@ -0,0 +1,17 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"github.com/crusttech/crust/auth/rest/handlers"
|
||||
"github.com/crusttech/crust/auth/service"
|
||||
"github.com/crusttech/crust/auth/types"
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
func MountRoutes(jwtAuth types.TokenEncoder) func(chi.Router) {
|
||||
var userSvc = service.User()
|
||||
|
||||
// Initialize handers & controllers.
|
||||
return func(r chi.Router) {
|
||||
handlers.NewAuth(Auth{}.New(userSvc, jwtAuth)).MountRoutes(r)
|
||||
}
|
||||
}
|
||||
48
auth/routes.go
Normal file
48
auth/routes.go
Normal file
@ -0,0 +1,48 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
)
|
||||
|
||||
func mountRoutes(r chi.Router, opts *configuration, mounts ...func(r chi.Router)) {
|
||||
if opts.http.logging {
|
||||
r.Use(middleware.Logger)
|
||||
}
|
||||
if opts.http.metrics {
|
||||
r.Use(metrics{}.Middleware("crm"))
|
||||
}
|
||||
|
||||
for _, mount := range mounts {
|
||||
mount(r)
|
||||
}
|
||||
}
|
||||
|
||||
func mountSystemRoutes(r chi.Router, opts *configuration) {
|
||||
if opts.http.metrics {
|
||||
r.Handle("/metrics", metrics{}.Handler())
|
||||
}
|
||||
r.Mount("/debug", middleware.Profiler())
|
||||
}
|
||||
|
||||
func printRoutes(r chi.Router, opts *configuration) {
|
||||
var printRoutes func(chi.Routes, string, string)
|
||||
printRoutes = func(r chi.Routes, indent string, prefix string) {
|
||||
routes := r.Routes()
|
||||
for _, route := range routes {
|
||||
if route.SubRoutes != nil && len(route.SubRoutes.Routes()) > 0 {
|
||||
fmt.Printf(indent+"%s - with %d handlers, %d subroutes\n", route.Pattern, len(route.Handlers), len(route.SubRoutes.Routes()))
|
||||
printRoutes(route.SubRoutes, indent+"\t", prefix+route.Pattern[:len(route.Pattern)-2])
|
||||
} else {
|
||||
for key, fn := range route.Handlers {
|
||||
fmt.Printf("%s%s\t%s -> %s\n", indent, key, prefix+route.Pattern, runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
printRoutes(r, "", "")
|
||||
}
|
||||
9
auth/service/error.go
Normal file
9
auth/service/error.go
Normal file
@ -0,0 +1,9 @@
|
||||
package service
|
||||
|
||||
type (
|
||||
serviceError string
|
||||
)
|
||||
|
||||
func (e serviceError) Error() string {
|
||||
return "crust.sam.service." + string(e)
|
||||
}
|
||||
98
auth/service/user.go
Normal file
98
auth/service/user.go
Normal file
@ -0,0 +1,98 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/crusttech/crust/auth/repository"
|
||||
"github.com/crusttech/crust/auth/types"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrUserInvalidCredentials = serviceError("UserInvalidCredentials")
|
||||
ErrUserLocked = serviceError("UserLocked")
|
||||
)
|
||||
|
||||
type (
|
||||
user struct {
|
||||
repository repository.User
|
||||
}
|
||||
|
||||
UserService interface {
|
||||
With(ctx context.Context) UserService
|
||||
|
||||
Create(input *types.User) (*types.User, error)
|
||||
ValidateCredentials(username, password string) (*types.User, error)
|
||||
}
|
||||
)
|
||||
|
||||
func User() UserService {
|
||||
return &user{
|
||||
repository.NewUser(context.Background()),
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *user) With(ctx context.Context) UserService {
|
||||
return &user{
|
||||
svc.repository.With(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *user) ValidateCredentials(username, password string) (*types.User, error) {
|
||||
user, err := svc.repository.FindUserByUsername(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !user.ValidatePassword(password) {
|
||||
return nil, ErrUserInvalidCredentials
|
||||
}
|
||||
|
||||
if !svc.canLogin(user) {
|
||||
return nil, ErrUserLocked
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (svc *user) FindByID(id uint64) (*types.User, error) {
|
||||
return svc.repository.FindUserByID(id)
|
||||
}
|
||||
|
||||
func (svc *user) Find(filter *types.UserFilter) ([]*types.User, error) {
|
||||
return svc.repository.FindUsers(filter)
|
||||
}
|
||||
|
||||
func (svc *user) Create(input *types.User) (user *types.User, err error) {
|
||||
return user, svc.repository.DB().Transaction(func() error {
|
||||
// Encrypt user password
|
||||
if user, err = svc.repository.CreateUser(input); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (svc *user) Update(mod *types.User) (*types.User, error) {
|
||||
return svc.repository.UpdateUser(mod)
|
||||
}
|
||||
|
||||
func (svc *user) canLogin(u *types.User) bool {
|
||||
return u != nil && u.ID > 0 && u.SuspendedAt == nil && u.DeletedAt == nil
|
||||
}
|
||||
|
||||
func (svc *user) Delete(id uint64) error {
|
||||
// @todo: permissions check if current user can delete this user
|
||||
// @todo: notify users that user has been removed (remove from web UI)
|
||||
return svc.repository.DeleteUserByID(id)
|
||||
}
|
||||
|
||||
func (svc *user) Suspend(id uint64) error {
|
||||
// @todo: permissions check if current user can suspend this user
|
||||
// @todo: notify users that user has been supsended (remove from web UI)
|
||||
return svc.repository.SuspendUserByID(id)
|
||||
}
|
||||
|
||||
func (svc *user) Unsuspend(id uint64) error {
|
||||
// @todo: permissions check if current user can unsuspend this user
|
||||
// @todo: notify users that user has been unsuspended
|
||||
return svc.repository.UnsuspendUserByID(id)
|
||||
}
|
||||
95
auth/start.go
Normal file
95
auth/start.go
Normal file
@ -0,0 +1,95 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/SentimensRG/ctx/sigctx"
|
||||
|
||||
"github.com/crusttech/crust/auth/rest"
|
||||
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/titpetric/factory"
|
||||
"github.com/titpetric/factory/resputil"
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
// validate configuration
|
||||
if err := config.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start/configure database connection
|
||||
factory.Database.Add("default", config.db.dsn)
|
||||
db, err := factory.Database.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// @todo: profiling as an external service?
|
||||
switch config.db.profiler {
|
||||
case "stdout":
|
||||
db.Profiler = &factory.Database.ProfilerStdout
|
||||
default:
|
||||
fmt.Println("No database query profiler selected")
|
||||
}
|
||||
|
||||
// configure resputil options
|
||||
resputil.SetConfig(resputil.Options{
|
||||
Pretty: config.http.pretty,
|
||||
Trace: config.http.tracing,
|
||||
Logger: func(err error) {
|
||||
// @todo: error logging
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Start() error {
|
||||
var ctx = sigctx.New()
|
||||
|
||||
log.Println("Starting http server on address " + config.http.addr)
|
||||
listener, err := net.Listen("tcp", config.http.addr)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("Can't listen on addr %s", config.http.addr))
|
||||
}
|
||||
|
||||
// JWT Auth
|
||||
jwtAuth, err := JWT()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error creating JWT Auth object")
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(handleCORS)
|
||||
|
||||
// Only protect application routes with JWT
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtAuth.Verifier(), jwtAuth.Authenticator())
|
||||
mountRoutes(r, config, rest.MountRoutes(jwtAuth))
|
||||
})
|
||||
|
||||
printRoutes(r, config)
|
||||
mountSystemRoutes(r, config)
|
||||
|
||||
go http.Serve(listener, r)
|
||||
<-ctx.Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sets up default CORS rules to use as a middleware
|
||||
func handleCORS(next http.Handler) http.Handler {
|
||||
return cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
}).Handler(next)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package auth
|
||||
package types
|
||||
|
||||
type (
|
||||
Identifiable interface {
|
||||
48
auth/types/user.go
Normal file
48
auth/types/user.go
Normal file
@ -0,0 +1,48 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
User struct {
|
||||
ID uint64 `json:"id" db:"id"`
|
||||
Username string `json:"username" db:"username"`
|
||||
Meta json.RawMessage `json:"-" db:"meta"`
|
||||
OrganisationID uint64 `json:"organisationId" db:"rel_organisation"`
|
||||
Password []byte `json:"-" db:"password"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty" db:"updated_at"`
|
||||
SuspendedAt *time.Time `json:"suspendedAt,omitempty" db:"suspended_at"`
|
||||
DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"`
|
||||
}
|
||||
|
||||
UserFilter struct {
|
||||
Query string
|
||||
MembersOfChannel uint64
|
||||
}
|
||||
)
|
||||
|
||||
func (u *User) Valid() bool {
|
||||
return u.ID > 0 && u.SuspendedAt == nil && u.DeletedAt == nil
|
||||
}
|
||||
|
||||
func (u *User) Identity() uint64 {
|
||||
return u.ID
|
||||
}
|
||||
|
||||
func (u *User) ValidatePassword(password string) bool {
|
||||
return bcrypt.CompareHashAndPassword(u.Password, []byte(password)) == nil
|
||||
}
|
||||
|
||||
func (u *User) GeneratePassword(password string) error {
|
||||
pwd, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.Password = pwd
|
||||
return nil
|
||||
}
|
||||
9
cmd/auth/dump.go
Normal file
9
cmd/auth/dump.go
Normal file
@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
// this file exists to keep go-spew in vendor for development needs
|
||||
|
||||
import (
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
var _ = spew.Dump
|
||||
23
cmd/auth/flags.go
Normal file
23
cmd/auth/flags.go
Normal file
@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"github.com/namsral/flag"
|
||||
)
|
||||
|
||||
type configuration struct {
|
||||
monitorInterval int
|
||||
}
|
||||
|
||||
func flags(prefix string, mountFlags ...func(...string)) configuration {
|
||||
var config configuration
|
||||
|
||||
flag.IntVar(&config.monitorInterval, "monitor-interval", 300, "Monitor interval (seconds, 0 = disable)")
|
||||
|
||||
for _, mount := range mountFlags {
|
||||
mount(prefix)
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
return config
|
||||
}
|
||||
25
cmd/auth/main.go
Normal file
25
cmd/auth/main.go
Normal file
@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/crusttech/crust/auth"
|
||||
"github.com/crusttech/crust/rbac"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := flags("auth", rbac.Flags, auth.Flags)
|
||||
|
||||
// log to stdout not stderr
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
go NewMonitor(config.monitorInterval)
|
||||
|
||||
if err := auth.Init(); err != nil {
|
||||
log.Fatalf("Error initializing auth: %+v", err)
|
||||
}
|
||||
if err := auth.Start(); err != nil {
|
||||
log.Fatalf("Error starting/running auth: %+v", err)
|
||||
}
|
||||
}
|
||||
59
cmd/auth/monitor.go
Normal file
59
cmd/auth/monitor.go
Normal file
@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Monitor struct {
|
||||
Alloc,
|
||||
TotalAlloc,
|
||||
Sys,
|
||||
Mallocs,
|
||||
Frees,
|
||||
LiveObjects,
|
||||
PauseTotalNs uint64
|
||||
|
||||
NumGC uint32
|
||||
NumGoroutine int
|
||||
}
|
||||
|
||||
func NewMonitor(duration int) {
|
||||
var (
|
||||
m = Monitor{}
|
||||
rtm runtime.MemStats
|
||||
goroutines = expvar.NewInt("num_goroutine")
|
||||
)
|
||||
var interval = time.Duration(duration) * time.Second
|
||||
for {
|
||||
<-time.After(interval)
|
||||
|
||||
// Read full mem stats
|
||||
runtime.ReadMemStats(&rtm)
|
||||
|
||||
// Number of goroutines
|
||||
m.NumGoroutine = runtime.NumGoroutine()
|
||||
goroutines.Set(int64(m.NumGoroutine))
|
||||
|
||||
// Misc memory stats
|
||||
m.Alloc = rtm.Alloc
|
||||
m.TotalAlloc = rtm.TotalAlloc
|
||||
m.Sys = rtm.Sys
|
||||
m.Mallocs = rtm.Mallocs
|
||||
m.Frees = rtm.Frees
|
||||
|
||||
// Live objects = Mallocs - Frees
|
||||
m.LiveObjects = m.Mallocs - m.Frees
|
||||
|
||||
// GC Stats
|
||||
m.PauseTotalNs = rtm.PauseTotalNs
|
||||
m.NumGC = rtm.NumGC
|
||||
|
||||
// Just encode to json and print
|
||||
b, _ := json.Marshal(m)
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
}
|
||||
36
codegen/auth/rest/handlers/index.php
Normal file
36
codegen/auth/rest/handlers/index.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
$templates = array(
|
||||
"http_handlers_inline.tpl" => function($name, $api) {
|
||||
return strtolower($name) . ".go";
|
||||
},
|
||||
);
|
||||
|
||||
foreach ($templates as $template => $fn)
|
||||
foreach ($apis as $api) {
|
||||
$name = ucfirst($api['interface']);
|
||||
$filename = $dirname . "/" . $fn($name, $api);
|
||||
|
||||
$tpl->load($template);
|
||||
$tpl->assign($common);
|
||||
$tpl->assign("package", basename(__DIR__));
|
||||
$tpl->assign("name", $name);
|
||||
$tpl->assign("api", $api);
|
||||
$tpl->assign("apis", $apis);
|
||||
$tpl->assign("self", strtolower(substr($name, 0, 1)));
|
||||
$tpl->assign("structs", $api['struct']);
|
||||
$imports = array();
|
||||
if (is_array($api['struct']))
|
||||
foreach ($api['struct'] as $struct) {
|
||||
if (isset($struct['imports']))
|
||||
foreach ($struct['imports'] as $import) {
|
||||
$imports[] = $import;
|
||||
}
|
||||
}
|
||||
$tpl->assign("imports", $imports);
|
||||
$tpl->assign("calls", $api['apis']);
|
||||
$contents = str_replace("\n\n}", "\n}", $tpl->get());
|
||||
|
||||
file_put_contents($filename, $contents);
|
||||
echo $filename . "\n";
|
||||
}
|
||||
30
codegen/auth/rest/index.php
Normal file
30
codegen/auth/rest/index.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
foreach ($apis as $api) {
|
||||
$name = ucfirst($api['interface']);
|
||||
$filename = $dirname . "/" . str_replace("..", ".", strtolower($name) . ".go");
|
||||
|
||||
$tpl->load("http_.tpl");
|
||||
$tpl->assign($common);
|
||||
$tpl->assign("package", basename(__DIR__));
|
||||
$tpl->assign("name", $name);
|
||||
$tpl->assign("api", $api);
|
||||
$tpl->assign("self", strtolower(substr($name, 0, 1)));
|
||||
$tpl->assign("structs", $api['struct']);
|
||||
$imports = array();
|
||||
if (is_array($api['struct']))
|
||||
foreach ($api['struct'] as $struct) {
|
||||
if (isset($struct['imports']))
|
||||
foreach ($struct['imports'] as $import) {
|
||||
$imports[] = $import;
|
||||
}
|
||||
}
|
||||
$tpl->assign("imports", $imports);
|
||||
$tpl->assign("calls", $api['apis']);
|
||||
$contents = str_replace("\n\n}", "\n}", $tpl->get());
|
||||
|
||||
if (!file_exists($filename)) {
|
||||
file_put_contents($filename, $contents);
|
||||
echo $filename . "\n";
|
||||
}
|
||||
}
|
||||
36
codegen/auth/rest/request/index.php
Normal file
36
codegen/auth/rest/request/index.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
$templates = array(
|
||||
"http_request_inline.tpl" => function($name, $api) {
|
||||
return strtolower($name) . ".go";
|
||||
},
|
||||
);
|
||||
|
||||
foreach ($templates as $template => $fn)
|
||||
foreach ($apis as $api) {
|
||||
$name = ucfirst($api['interface']);
|
||||
$filename = $dirname . "/" . $fn($name, $api);
|
||||
|
||||
$tpl->load($template);
|
||||
$tpl->assign($common);
|
||||
$tpl->assign("package", basename(__DIR__));
|
||||
$tpl->assign("name", $name);
|
||||
$tpl->assign("api", $api);
|
||||
$tpl->assign("apis", $apis);
|
||||
$tpl->assign("self", strtolower(substr($name, 0, 1)));
|
||||
$tpl->assign("structs", $api['struct']);
|
||||
$imports = array();
|
||||
if (is_array($api['struct']))
|
||||
foreach ($api['struct'] as $struct) {
|
||||
if (isset($struct['imports']))
|
||||
foreach ($struct['imports'] as $import) {
|
||||
$imports[] = $import;
|
||||
}
|
||||
}
|
||||
$tpl->assign("imports", $imports);
|
||||
$tpl->assign("calls", $api['apis']);
|
||||
$contents = str_replace("\n\n}", "\n}", $tpl->get());
|
||||
|
||||
file_put_contents($filename, $contents);
|
||||
echo $filename . "\n";
|
||||
}
|
||||
37
codegen/auth/types/index.php
Normal file
37
codegen/auth/types/index.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
$templates = array(
|
||||
"http_structs.tpl" => function($name, $api) {
|
||||
return strtolower($name) . ".go";
|
||||
},
|
||||
);
|
||||
|
||||
foreach ($templates as $template => $fn)
|
||||
foreach ($apis as $api) {
|
||||
if (is_array($api['struct'])) {
|
||||
$name = ucfirst($api['interface']);
|
||||
$filename = $dirname . "/" . $fn($name, $api);
|
||||
|
||||
$tpl->load($template);
|
||||
$tpl->assign($common);
|
||||
$tpl->assign("package", basename(__DIR__));
|
||||
$tpl->assign("name", $name);
|
||||
$tpl->assign("api", $api);
|
||||
$tpl->assign("apis", $apis);
|
||||
$tpl->assign("self", strtolower(substr($name, 0, 1)));
|
||||
$tpl->assign("structs", $api['struct']);
|
||||
$imports = array();
|
||||
foreach ($api['struct'] as $struct) {
|
||||
if (isset($struct['imports']))
|
||||
foreach ($struct['imports'] as $import) {
|
||||
$imports[] = $import;
|
||||
}
|
||||
}
|
||||
$tpl->assign("imports", $imports);
|
||||
$tpl->assign("calls", $api['apis']);
|
||||
$contents = str_replace("\n\n}", "\n}", $tpl->get());
|
||||
|
||||
file_put_contents($filename, $contents);
|
||||
echo $filename . "\n";
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user