Add system/applications
- CRUD support for applications - new (application related) permissions
This commit is contained in:
@@ -574,6 +574,95 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Applications",
|
||||
"path": "/application",
|
||||
"entrypoint": "application",
|
||||
"authentication": [],
|
||||
"struct": [
|
||||
{
|
||||
"imports": [
|
||||
"sqlxTypes github.com/jmoiron/sqlx/types"
|
||||
]
|
||||
}
|
||||
],
|
||||
"apis": [
|
||||
{
|
||||
"name": "list",
|
||||
"method": "GET",
|
||||
"title": "List applications",
|
||||
"path": "/"
|
||||
},
|
||||
{
|
||||
"name": "create",
|
||||
"method": "POST",
|
||||
"title": "Create application",
|
||||
"path": "/",
|
||||
"parameters": {
|
||||
"post": [
|
||||
{ "name": "name", "type": "string", "required": true, "title": "Email"},
|
||||
{ "name": "enabled", "type": "bool", "required": false, "title": "Enabled"},
|
||||
{ "name": "unify", "type": "sqlxTypes.JSONText", "required": false, "title": "Unify properties"},
|
||||
{ "name": "config", "type": "sqlxTypes.JSONText", "required": false, "title": "Arbitrary JSON holding application configuration"}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "update",
|
||||
"method": "PUT",
|
||||
"title": "Update user details",
|
||||
"path": "/{applicationID}",
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "applicationID",
|
||||
"required": true,
|
||||
"title": "Application ID"
|
||||
}
|
||||
],
|
||||
"post": [
|
||||
{ "name": "name", "type": "string", "required": true, "title": "Email"},
|
||||
{ "name": "enabled", "type": "bool", "required": false, "title": "Enabled"},
|
||||
{ "name": "unify", "type": "sqlxTypes.JSONText", "required": false, "title": "Unify properties"},
|
||||
{ "name": "config", "type": "sqlxTypes.JSONText", "required": false, "title": "Arbitrary JSON holding application configuration"}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "read",
|
||||
"method": "GET",
|
||||
"title": "Read application details",
|
||||
"path": "/{applicationID}",
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "applicationID",
|
||||
"required": true,
|
||||
"title": "Application ID"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "delete",
|
||||
"method": "DELETE",
|
||||
"title": "Remove application",
|
||||
"path": "/{applicationID}",
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "applicationID",
|
||||
"required": true,
|
||||
"title": "Application ID"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Permissions",
|
||||
"parameters": {},
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"Title": "Applications",
|
||||
"Interface": "Application",
|
||||
"Struct": [
|
||||
{
|
||||
"imports": [
|
||||
"sqlxTypes github.com/jmoiron/sqlx/types"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Parameters": null,
|
||||
"Protocol": "",
|
||||
"Authentication": [],
|
||||
"Path": "/application",
|
||||
"APIs": [
|
||||
{
|
||||
"Name": "list",
|
||||
"Method": "GET",
|
||||
"Title": "List applications",
|
||||
"Path": "/",
|
||||
"Parameters": null
|
||||
},
|
||||
{
|
||||
"Name": "create",
|
||||
"Method": "POST",
|
||||
"Title": "Create application",
|
||||
"Path": "/",
|
||||
"Parameters": {
|
||||
"post": [
|
||||
{
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"title": "Email",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"required": false,
|
||||
"title": "Enabled",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "unify",
|
||||
"required": false,
|
||||
"title": "Unify properties",
|
||||
"type": "sqlxTypes.JSONText"
|
||||
},
|
||||
{
|
||||
"name": "config",
|
||||
"required": false,
|
||||
"title": "Arbitrary JSON holding application configuration",
|
||||
"type": "sqlxTypes.JSONText"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "update",
|
||||
"Method": "PUT",
|
||||
"Title": "Update user details",
|
||||
"Path": "/{applicationID}",
|
||||
"Parameters": {
|
||||
"path": [
|
||||
{
|
||||
"name": "applicationID",
|
||||
"required": true,
|
||||
"title": "Application ID",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"post": [
|
||||
{
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"title": "Email",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"required": false,
|
||||
"title": "Enabled",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "unify",
|
||||
"required": false,
|
||||
"title": "Unify properties",
|
||||
"type": "sqlxTypes.JSONText"
|
||||
},
|
||||
{
|
||||
"name": "config",
|
||||
"required": false,
|
||||
"title": "Arbitrary JSON holding application configuration",
|
||||
"type": "sqlxTypes.JSONText"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "read",
|
||||
"Method": "GET",
|
||||
"Title": "Read application details",
|
||||
"Path": "/{applicationID}",
|
||||
"Parameters": {
|
||||
"path": [
|
||||
{
|
||||
"name": "applicationID",
|
||||
"required": true,
|
||||
"title": "Application ID",
|
||||
"type": "uint64"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "delete",
|
||||
"Method": "DELETE",
|
||||
"Title": "Remove application",
|
||||
"Path": "/{applicationID}",
|
||||
"Parameters": {
|
||||
"path": [
|
||||
{
|
||||
"name": "applicationID",
|
||||
"required": true,
|
||||
"title": "Application ID",
|
||||
"type": "uint64"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -39,6 +39,7 @@ function types {
|
||||
./build/gen-type-set --with-primary-key=false --types Unread --output messaging/types/unread.gen.go
|
||||
|
||||
./build/gen-type-set --types User --output system/types/user.gen.go
|
||||
./build/gen-type-set --types Application --output system/types/application.gen.go
|
||||
./build/gen-type-set --with-resources=true --resource-type "rules.Resource" --imports "github.com/crusttech/crust/internal/rules" --types Role --output system/types/role.gen.go
|
||||
./build/gen-type-set --with-resources=true --resource-type "rules.Resource" --imports "github.com/crusttech/crust/internal/rules" --types Organisation --output system/types/organisation.gen.go
|
||||
./build/gen-type-set --types Credentials --output system/types/credentials.gen.go
|
||||
|
||||
@@ -1,3 +1,84 @@
|
||||
# Applications
|
||||
|
||||
## List applications
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/application/` | HTTP/S | GET | |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
|
||||
## Create application
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/application/` | HTTP/S | POST | |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| name | string | POST | Email | N/A | YES |
|
||||
| enabled | bool | POST | Enabled | N/A | NO |
|
||||
| unify | sqlxTypes.JSONText | POST | Unify properties | N/A | NO |
|
||||
| config | sqlxTypes.JSONText | POST | Arbitrary JSON holding application configuration | N/A | NO |
|
||||
|
||||
## Update user details
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/application/{applicationID}` | HTTP/S | PUT | |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| applicationID | uint64 | PATH | Application ID | N/A | YES |
|
||||
| name | string | POST | Email | N/A | YES |
|
||||
| enabled | bool | POST | Enabled | N/A | NO |
|
||||
| unify | sqlxTypes.JSONText | POST | Unify properties | N/A | NO |
|
||||
| config | sqlxTypes.JSONText | POST | Arbitrary JSON holding application configuration | N/A | NO |
|
||||
|
||||
## Read application details
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/application/{applicationID}` | HTTP/S | GET | |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| applicationID | uint64 | PATH | Application ID | N/A | YES |
|
||||
|
||||
## Remove application
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/application/{applicationID}` | HTTP/S | DELETE | |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| applicationID | uint64 | PATH | Application ID | N/A | YES |
|
||||
|
||||
|
||||
|
||||
|
||||
# Authentication
|
||||
|
||||
## Check JWT token
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,40 @@
|
||||
CREATE TABLE sys_application (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),
|
||||
name TEXT NOT NULL COMMENT 'something we can differentiate application by',
|
||||
enabled BOOL NOT NULL,
|
||||
|
||||
unify JSON NULL COMMENT 'unify specific settings',
|
||||
|
||||
created_at DATETIME NOT NULL DEFAULT NOW(),
|
||||
updated_at DATETIME NULL,
|
||||
deleted_at DATETIME NULL, -- user soft delete
|
||||
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
|
||||
REPLACE INTO `sys_application` (`id`, `name`, `enabled`, `rel_owner`, `unify`) VALUES
|
||||
( 1, 'Crust Messaging', true, 0,
|
||||
'{"logo": "/applications/crust.jpg", "icon": "/applications/crust_favicon.png", "url": "/messaging/", "listed": true}'
|
||||
),
|
||||
( 2, 'Crust CRM', true, 0,
|
||||
'{"logo": "/applications/crust.jpg", "icon": "/applications/crust_favicon.png", "url": "/crm/", "listed": true}'
|
||||
),
|
||||
( 3, 'Crust Admin Area', true, 0,
|
||||
'{"logo": "/applications/crust.jpg", "icon": "/applications/crust_favicon.png", "url": "/admin/", "listed": true}'
|
||||
),
|
||||
( 4, 'Corteza Jitsi Bridge', true, 0,
|
||||
'{"logo": "/applications/jitsi.png", "icon": "/applications/jitsi_icon.png", "url": "/bridge/jitsi/", "listed": true}'
|
||||
),
|
||||
( 5, 'Google Maps', true, 0,
|
||||
'{"logo": "/applications/google_maps.png", "icon": "/applications/google_maps_icon.png", "url": "/bridge/google-maps/", "listed": true}'
|
||||
);
|
||||
|
||||
-- Allow admin access to applications
|
||||
INSERT INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES
|
||||
(2, 'system', 'application.create', 2),
|
||||
(2, 'application:role', 'read', 2),
|
||||
(2, 'application:role', 'update', 2),
|
||||
(2, 'application:role', 'delete', 2)
|
||||
;
|
||||
@@ -0,0 +1,85 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/titpetric/factory"
|
||||
|
||||
"github.com/crusttech/crust/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
ApplicationRepository interface {
|
||||
With(ctx context.Context, db *factory.DB) ApplicationRepository
|
||||
|
||||
FindByID(id uint64) (*types.Application, error)
|
||||
Find() (types.ApplicationSet, error)
|
||||
|
||||
Create(mod *types.Application) (*types.Application, error)
|
||||
Update(mod *types.Application) (*types.Application, error)
|
||||
|
||||
DeleteByID(id uint64) error
|
||||
}
|
||||
|
||||
application struct {
|
||||
*repository
|
||||
|
||||
// sql table reference
|
||||
applications string
|
||||
members string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
sqlApplicationColumns = "id, rel_owner, name, enabled, unify, created_at, updated_at, deleted_at"
|
||||
sqlApplicationScope = "deleted_at IS NULL"
|
||||
|
||||
ErrApplicationNotFound = repositoryError("ApplicationNotFound")
|
||||
)
|
||||
|
||||
func Application(ctx context.Context, db *factory.DB) ApplicationRepository {
|
||||
return (&application{}).With(ctx, db)
|
||||
}
|
||||
|
||||
func (r *application) With(ctx context.Context, db *factory.DB) ApplicationRepository {
|
||||
return &application{
|
||||
repository: r.repository.With(ctx, db),
|
||||
applications: "sys_application",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *application) FindByID(id uint64) (*types.Application, error) {
|
||||
sql := "SELECT " + sqlApplicationColumns + " FROM " + r.applications + " WHERE id = ? AND " + sqlApplicationScope
|
||||
mod := &types.Application{}
|
||||
|
||||
return mod, isFound(r.db().Get(mod, sql, id), mod.ID > 0, ErrApplicationNotFound)
|
||||
}
|
||||
|
||||
func (r *application) Find() (types.ApplicationSet, error) {
|
||||
rval := make([]*types.Application, 0)
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
sql := "SELECT " + sqlApplicationColumns + " FROM " + r.applications + " WHERE " + sqlApplicationScope
|
||||
|
||||
sql += " ORDER BY id ASC"
|
||||
|
||||
return rval, r.db().Select(&rval, sql, params...)
|
||||
}
|
||||
|
||||
func (r *application) Create(mod *types.Application) (*types.Application, error) {
|
||||
mod.ID = factory.Sonyflake.NextID()
|
||||
mod.CreatedAt = time.Now()
|
||||
|
||||
return mod, r.db().Insert(r.applications, mod)
|
||||
}
|
||||
|
||||
func (r *application) Update(mod *types.Application) (*types.Application, error) {
|
||||
mod.UpdatedAt = timeNowPtr()
|
||||
|
||||
return mod, r.db().Replace(r.applications, mod)
|
||||
}
|
||||
|
||||
func (r *application) DeleteByID(id uint64) error {
|
||||
return r.updateColumnByID(r.applications, "deleted_at", time.Now(), id)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/titpetric/factory"
|
||||
|
||||
"github.com/crusttech/crust/internal/test"
|
||||
"github.com/crusttech/crust/system/types"
|
||||
)
|
||||
|
||||
func TestApplication(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
return
|
||||
}
|
||||
|
||||
crepo := Application(context.Background(), factory.Database.MustGet())
|
||||
|
||||
{
|
||||
|
||||
tx(t, func() (err error) {
|
||||
if _, err = factory.Database.MustGet().Exec("TRUNCATE sys_application"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
app := &types.Application{
|
||||
Name: "created",
|
||||
Enabled: true,
|
||||
OwnerID: 1,
|
||||
Unify: &types.ApplicationUnify{
|
||||
Name: "created",
|
||||
Listed: true,
|
||||
Order: 1,
|
||||
Icon: "...ico",
|
||||
},
|
||||
}
|
||||
|
||||
app, err = crepo.Create(app)
|
||||
test.NoError(t, err, "Application.Create error: %+v", err)
|
||||
test.Assert(t, app.Valid(), "Expecting application to be valid after creation")
|
||||
test.Assert(t, app.Name == "created", "Expecting application name to be set, got %q", app.Name)
|
||||
test.Assert(t, app.Enabled, "Expecting application to be enabled")
|
||||
test.Assert(t, app.Unify.Name == "created", "Expecting application name to be set in unify, got %q", app.Name)
|
||||
test.Assert(t, app.Unify.Listed, "Expecting application to be listed in unify")
|
||||
test.Assert(t, app.Unify.Order == 1, "Expecting application name to have order val 1")
|
||||
|
||||
app.Name = "updated"
|
||||
app.Enabled = false
|
||||
app.Unify.Name = "updated"
|
||||
app.Unify.Listed = false
|
||||
app, err = crepo.Update(app)
|
||||
|
||||
test.NoError(t, err, "Application.Create error: %+v", err)
|
||||
test.Assert(t, err == nil, "Application.Create error: %+v", err)
|
||||
test.Assert(t, app.Name == "updated", "Expecting application name to be updated")
|
||||
test.Assert(t, !app.Enabled, "Expecting application to be disabled")
|
||||
test.Assert(t, app.Unify.Name == "updated", "Expecting application name to be updated in unify")
|
||||
test.Assert(t, !app.Unify.Listed, "Expecting application to be unlisted in unify")
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/crusttech/crust/system/rest/request"
|
||||
"github.com/crusttech/crust/system/service"
|
||||
"github.com/crusttech/crust/system/types"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var _ = errors.Wrap
|
||||
|
||||
type Application struct {
|
||||
svc struct {
|
||||
application service.ApplicationService
|
||||
}
|
||||
}
|
||||
|
||||
func (Application) New() *Application {
|
||||
ctrl := &Application{}
|
||||
ctrl.svc.application = service.DefaultApplication
|
||||
|
||||
return ctrl
|
||||
}
|
||||
|
||||
func (ctrl *Application) List(ctx context.Context, r *request.ApplicationList) (interface{}, error) {
|
||||
return ctrl.svc.application.With(ctx).Find()
|
||||
}
|
||||
|
||||
func (ctrl *Application) Create(ctx context.Context, r *request.ApplicationCreate) (interface{}, error) {
|
||||
app := &types.Application{
|
||||
Name: r.Name,
|
||||
Enabled: r.Enabled,
|
||||
}
|
||||
|
||||
if r.Unify != nil {
|
||||
app.Unify = &types.ApplicationUnify{}
|
||||
if err := r.Unify.Unmarshal(app.Unify); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ctrl.svc.application.With(ctx).Create(app)
|
||||
}
|
||||
|
||||
func (ctrl *Application) Update(ctx context.Context, r *request.ApplicationUpdate) (interface{}, error) {
|
||||
app := &types.Application{
|
||||
ID: r.ApplicationID,
|
||||
Name: r.Name,
|
||||
Enabled: r.Enabled,
|
||||
}
|
||||
|
||||
if r.Unify != nil {
|
||||
app.Unify = &types.ApplicationUnify{}
|
||||
if err := r.Unify.Unmarshal(app.Unify); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ctrl.svc.application.With(ctx).Update(app)
|
||||
}
|
||||
|
||||
func (ctrl *Application) Read(ctx context.Context, r *request.ApplicationRead) (interface{}, error) {
|
||||
return ctrl.svc.application.With(ctx).FindByID(r.ApplicationID)
|
||||
}
|
||||
|
||||
func (ctrl *Application) Delete(ctx context.Context, r *request.ApplicationDelete) (interface{}, error) {
|
||||
return nil, ctrl.svc.application.With(ctx).DeleteByID(r.ApplicationID)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
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 `application.go`, `application.util.go` or `application_test.go` to
|
||||
implement your API calls, helper functions and tests. The file `application.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/system/rest/request"
|
||||
)
|
||||
|
||||
// Internal API interface
|
||||
type ApplicationAPI interface {
|
||||
List(context.Context, *request.ApplicationList) (interface{}, error)
|
||||
Create(context.Context, *request.ApplicationCreate) (interface{}, error)
|
||||
Update(context.Context, *request.ApplicationUpdate) (interface{}, error)
|
||||
Read(context.Context, *request.ApplicationRead) (interface{}, error)
|
||||
Delete(context.Context, *request.ApplicationDelete) (interface{}, error)
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
type Application struct {
|
||||
List func(http.ResponseWriter, *http.Request)
|
||||
Create func(http.ResponseWriter, *http.Request)
|
||||
Update func(http.ResponseWriter, *http.Request)
|
||||
Read func(http.ResponseWriter, *http.Request)
|
||||
Delete func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func NewApplication(ah ApplicationAPI) *Application {
|
||||
return &Application{
|
||||
List: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewApplicationList()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return ah.List(r.Context(), params)
|
||||
})
|
||||
},
|
||||
Create: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewApplicationCreate()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return ah.Create(r.Context(), params)
|
||||
})
|
||||
},
|
||||
Update: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewApplicationUpdate()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return ah.Update(r.Context(), params)
|
||||
})
|
||||
},
|
||||
Read: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewApplicationRead()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return ah.Read(r.Context(), params)
|
||||
})
|
||||
},
|
||||
Delete: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewApplicationDelete()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return ah.Delete(r.Context(), params)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (ah *Application) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middlewares...)
|
||||
r.Route("/application", func(r chi.Router) {
|
||||
r.Get("/", ah.List)
|
||||
r.Post("/", ah.Create)
|
||||
r.Put("/{applicationID}", ah.Update)
|
||||
r.Get("/{applicationID}", ah.Read)
|
||||
r.Delete("/{applicationID}", ah.Delete)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
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 `application.go`, `application.util.go` or `application_test.go` to
|
||||
implement your API calls, helper functions and tests. The file `application.go`
|
||||
is only generated the first time, and will not be overwritten if it exists.
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
sqlxTypes "github.com/jmoiron/sqlx/types"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
// Application list request parameters
|
||||
type ApplicationList struct {
|
||||
}
|
||||
|
||||
func NewApplicationList() *ApplicationList {
|
||||
return &ApplicationList{}
|
||||
}
|
||||
|
||||
func (apReq *ApplicationList) Fill(r *http.Request) (err error) {
|
||||
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(r.Body).Decode(apReq)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewApplicationList()
|
||||
|
||||
// Application create request parameters
|
||||
type ApplicationCreate struct {
|
||||
Name string
|
||||
Enabled bool
|
||||
Unify sqlxTypes.JSONText
|
||||
Config sqlxTypes.JSONText
|
||||
}
|
||||
|
||||
func NewApplicationCreate() *ApplicationCreate {
|
||||
return &ApplicationCreate{}
|
||||
}
|
||||
|
||||
func (apReq *ApplicationCreate) Fill(r *http.Request) (err error) {
|
||||
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(r.Body).Decode(apReq)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
apReq.Name = val
|
||||
}
|
||||
if val, ok := post["enabled"]; ok {
|
||||
|
||||
apReq.Enabled = parseBool(val)
|
||||
}
|
||||
if val, ok := post["unify"]; ok {
|
||||
|
||||
if apReq.Unify, err = parseJSONTextWithErr(val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := post["config"]; ok {
|
||||
|
||||
if apReq.Config, err = parseJSONTextWithErr(val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewApplicationCreate()
|
||||
|
||||
// Application update request parameters
|
||||
type ApplicationUpdate struct {
|
||||
ApplicationID uint64 `json:",string"`
|
||||
Name string
|
||||
Enabled bool
|
||||
Unify sqlxTypes.JSONText
|
||||
Config sqlxTypes.JSONText
|
||||
}
|
||||
|
||||
func NewApplicationUpdate() *ApplicationUpdate {
|
||||
return &ApplicationUpdate{}
|
||||
}
|
||||
|
||||
func (apReq *ApplicationUpdate) Fill(r *http.Request) (err error) {
|
||||
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(r.Body).Decode(apReq)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
apReq.ApplicationID = parseUInt64(chi.URLParam(r, "applicationID"))
|
||||
if val, ok := post["name"]; ok {
|
||||
|
||||
apReq.Name = val
|
||||
}
|
||||
if val, ok := post["enabled"]; ok {
|
||||
|
||||
apReq.Enabled = parseBool(val)
|
||||
}
|
||||
if val, ok := post["unify"]; ok {
|
||||
|
||||
if apReq.Unify, err = parseJSONTextWithErr(val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if val, ok := post["config"]; ok {
|
||||
|
||||
if apReq.Config, err = parseJSONTextWithErr(val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewApplicationUpdate()
|
||||
|
||||
// Application read request parameters
|
||||
type ApplicationRead struct {
|
||||
ApplicationID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
func NewApplicationRead() *ApplicationRead {
|
||||
return &ApplicationRead{}
|
||||
}
|
||||
|
||||
func (apReq *ApplicationRead) Fill(r *http.Request) (err error) {
|
||||
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(r.Body).Decode(apReq)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
apReq.ApplicationID = parseUInt64(chi.URLParam(r, "applicationID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewApplicationRead()
|
||||
|
||||
// Application delete request parameters
|
||||
type ApplicationDelete struct {
|
||||
ApplicationID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
func NewApplicationDelete() *ApplicationDelete {
|
||||
return &ApplicationDelete{}
|
||||
}
|
||||
|
||||
func (apReq *ApplicationDelete) Fill(r *http.Request) (err error) {
|
||||
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(r.Body).Decode(apReq)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
apReq.ApplicationID = parseUInt64(chi.URLParam(r, "applicationID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewApplicationDelete()
|
||||
@@ -50,6 +50,7 @@ func MountRoutes(oidcConfig *config.OIDC, socialConfig *config.Social, jwtEncode
|
||||
handlers.NewRole(Role{}.New()).MountRoutes(r)
|
||||
handlers.NewOrganisation(Organisation{}.New()).MountRoutes(r)
|
||||
handlers.NewPermissions(Permissions{}.New()).MountRoutes(r)
|
||||
handlers.NewApplication(Application{}.New()).MountRoutes(r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/titpetric/factory"
|
||||
|
||||
"github.com/crusttech/crust/system/repository"
|
||||
"github.com/crusttech/crust/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
application struct {
|
||||
db *factory.DB
|
||||
ctx context.Context
|
||||
|
||||
application repository.ApplicationRepository
|
||||
}
|
||||
|
||||
ApplicationService interface {
|
||||
With(ctx context.Context) ApplicationService
|
||||
|
||||
FindByID(applicationID uint64) (*types.Application, error)
|
||||
Find() (types.ApplicationSet, error)
|
||||
|
||||
Create(application *types.Application) (*types.Application, error)
|
||||
Update(application *types.Application) (*types.Application, error)
|
||||
DeleteByID(id uint64) error
|
||||
}
|
||||
)
|
||||
|
||||
func Application() ApplicationService {
|
||||
return (&application{}).With(context.Background())
|
||||
}
|
||||
|
||||
func (svc *application) With(ctx context.Context) ApplicationService {
|
||||
db := repository.DB(ctx)
|
||||
return &application{
|
||||
db: db,
|
||||
ctx: ctx,
|
||||
application: repository.Application(ctx, db),
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *application) FindByID(id uint64) (*types.Application, error) {
|
||||
// @todo: permission check if current user has access to this application
|
||||
return svc.application.FindByID(id)
|
||||
}
|
||||
|
||||
func (svc *application) Find() (types.ApplicationSet, error) {
|
||||
// @todo: permission check to return only applications that current user has access to
|
||||
return svc.application.Find()
|
||||
}
|
||||
|
||||
func (svc *application) Create(mod *types.Application) (*types.Application, error) {
|
||||
// @todo: permission check if current user can add/edit application
|
||||
|
||||
return svc.application.Create(mod)
|
||||
}
|
||||
|
||||
func (svc *application) Update(mod *types.Application) (t *types.Application, err error) {
|
||||
// @todo: permission check if current user can add/edit application
|
||||
// @todo: make sure archived & deleted entries can not be edited
|
||||
|
||||
return t, svc.db.Transaction(func() (err error) {
|
||||
if t, err = svc.application.FindByID(mod.ID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Assign changed values
|
||||
t.Name = mod.Name
|
||||
t.Enabled = mod.Enabled
|
||||
t.Unify = mod.Unify
|
||||
|
||||
if t, err = svc.application.Update(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (svc *application) DeleteByID(id uint64) error {
|
||||
// @todo: make history unavailable
|
||||
// @todo: notify users that application has been removed (remove from web UI)
|
||||
// @todo: permissions check if current user can remove application
|
||||
return svc.application.DeleteByID(id)
|
||||
}
|
||||
|
||||
var _ ApplicationService = &application{}
|
||||
@@ -17,6 +17,7 @@ var (
|
||||
DefaultRole RoleService
|
||||
DefaultPermissions PermissionsService
|
||||
DefaultOrganisation OrganisationService
|
||||
DefaultApplication ApplicationService
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -26,5 +27,6 @@ func Init() {
|
||||
DefaultRole = Role()
|
||||
DefaultPermissions = Permissions()
|
||||
DefaultOrganisation = Organisation()
|
||||
DefaultApplication = Application()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ var (
|
||||
"grant": true,
|
||||
"organisation.create": true,
|
||||
"role.create": true,
|
||||
"application.create": true,
|
||||
},
|
||||
"system:organisation": map[string]bool{
|
||||
"access": true,
|
||||
@@ -22,6 +23,11 @@ var (
|
||||
"delete": true,
|
||||
"members.manage": true,
|
||||
},
|
||||
"application:role": map[string]bool{
|
||||
"read": true,
|
||||
"update": true,
|
||||
"delete": true,
|
||||
},
|
||||
"messaging": map[string]bool{
|
||||
"access": true,
|
||||
"grant": true,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package types
|
||||
|
||||
// Hello! This file is auto-generated.
|
||||
|
||||
type (
|
||||
|
||||
// ApplicationSet slice of Application
|
||||
//
|
||||
// This type is auto-generated.
|
||||
ApplicationSet []*Application
|
||||
)
|
||||
|
||||
// Walk iterates through every slice item and calls w(Application) err
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set ApplicationSet) Walk(w func(*Application) error) (err error) {
|
||||
for i := range set {
|
||||
if err = w(set[i]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Filter iterates through every slice item, calls f(Application) (bool, err) and return filtered slice
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set ApplicationSet) Filter(f func(*Application) (bool, error)) (out ApplicationSet, err error) {
|
||||
var ok bool
|
||||
out = ApplicationSet{}
|
||||
for i := range set {
|
||||
if ok, err = f(set[i]); err != nil {
|
||||
return
|
||||
} else if ok {
|
||||
out = append(out, set[i])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// FindByID finds items from slice by its ID property
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set ApplicationSet) FindByID(ID uint64) *Application {
|
||||
for i := range set {
|
||||
if set[i].ID == ID {
|
||||
return set[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IDs returns a slice of uint64s from all items in the set
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set ApplicationSet) IDs() (IDs []uint64) {
|
||||
IDs = make([]uint64, len(set))
|
||||
|
||||
for i := range set {
|
||||
IDs[i] = set[i].ID
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
Application struct {
|
||||
ID uint64 `json:"applicationID,string" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
OwnerID uint64 `json:"ownerID" db:"rel_owner"`
|
||||
Enabled bool `json:"enabled" db:"enabled"`
|
||||
|
||||
Unify *ApplicationUnify `json:"unify,omitempty" db:"unify"`
|
||||
|
||||
CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty" db:"updated_at"`
|
||||
DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"`
|
||||
}
|
||||
|
||||
ApplicationUnify struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Listed bool `json:"listed"`
|
||||
Icon string `json:"icon"`
|
||||
Logo string `json:"logo"`
|
||||
Url string `json:"url"`
|
||||
Config string `json:"config"`
|
||||
Order uint `json:"order"`
|
||||
}
|
||||
)
|
||||
|
||||
func (u *Application) Valid() bool {
|
||||
return u.ID > 0 && u.DeletedAt == nil
|
||||
}
|
||||
|
||||
func (u *Application) Identity() uint64 {
|
||||
return u.ID
|
||||
}
|
||||
|
||||
func (au *ApplicationUnify) Scan(value interface{}) error {
|
||||
switch value.(type) {
|
||||
case nil:
|
||||
au = nil
|
||||
case []uint8:
|
||||
if err := json.Unmarshal(value.([]byte), au); err != nil {
|
||||
return errors.Wrapf(err, "Can not scan '%v' into ApplicationUnify", value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (au ApplicationUnify) Value() (driver.Value, error) {
|
||||
return json.Marshal(au)
|
||||
}
|
||||
Reference in New Issue
Block a user