init import
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package accoper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"mbase/pkg/auxpwd"
|
||||
"mbase/pkg/auxtool"
|
||||
"mbase/pkg/auxuuid"
|
||||
"mbase/pkg/descr"
|
||||
)
|
||||
|
||||
type CreateAccountParams struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
type CreateAccountResult struct {
|
||||
AccountID string `json:"accountId"`
|
||||
}
|
||||
|
||||
func (oper *Operator) CreateAccount(ctx context.Context, operatorID string, params *CreateAccountParams) (*CreateAccountResult, error) {
|
||||
var err error
|
||||
res := &CreateAccountResult{}
|
||||
|
||||
if params.Username == "" {
|
||||
err := fmt.Errorf("Empty username parameters")
|
||||
return res, err
|
||||
}
|
||||
|
||||
if params.Password == "" {
|
||||
err := fmt.Errorf("Empty password parameter")
|
||||
return res, err
|
||||
}
|
||||
|
||||
accountExists, _, err := oper.mdb.GetAccountByUsername(ctx, params.Username)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if accountExists {
|
||||
err := fmt.Errorf("Account with thist name already exists")
|
||||
return res, err
|
||||
}
|
||||
now := auxtool.TimeNow()
|
||||
passhash := auxpwd.MakeSHA256Hash([]byte(params.Password))
|
||||
accountDescr := &descr.Account{
|
||||
ID: auxuuid.NewUUID(),
|
||||
Username: params.Username,
|
||||
Passhash: passhash,
|
||||
Disabled: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
CreatedBy: operatorID,
|
||||
UpdatedBy: operatorID,
|
||||
}
|
||||
err = oper.mdb.InsertAccount(ctx, accountDescr)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
res.AccountID = accountDescr.ID
|
||||
return res, err
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package accoper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"mbase/pkg/auxtool"
|
||||
"mbase/pkg/auxuuid"
|
||||
"mbase/pkg/descr"
|
||||
)
|
||||
|
||||
// CreateGrant
|
||||
type CreateGrantParams struct {
|
||||
AccountID string `json:"accountID"`
|
||||
Username string `json:"username"`
|
||||
Right string `json:"operation"`
|
||||
Pattern string `json:"pattern"`
|
||||
}
|
||||
type CreateGrantResult struct {
|
||||
GrantID string `json:"grantId"`
|
||||
}
|
||||
|
||||
func (oper *Operator) CreateGrant(ctx context.Context, operatorID string, params *CreateGrantParams) (*CreateGrantResult, error) {
|
||||
var err error
|
||||
res := &CreateGrantResult{}
|
||||
|
||||
if params.AccountID == "" {
|
||||
err := fmt.Errorf("Empty accountId parameters")
|
||||
return res, err
|
||||
}
|
||||
if params.Right == "" {
|
||||
err := fmt.Errorf("Empty operation parameter")
|
||||
return res, err
|
||||
}
|
||||
if params.Pattern == "" {
|
||||
err := fmt.Errorf("Empty pattern parameter")
|
||||
return res, err
|
||||
}
|
||||
|
||||
_, err = regexp.Compile(params.Pattern)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Cannot compile regexp %s: %v", err)
|
||||
return res, err
|
||||
}
|
||||
|
||||
var accountDescr *descr.Account
|
||||
var accountExists bool
|
||||
switch {
|
||||
case params.AccountID != "":
|
||||
accountExists, accountDescr, err = oper.mdb.GetAccountByID(ctx, params.AccountID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if !accountExists {
|
||||
err := fmt.Errorf("Account with ID %s dont exists", params.AccountID)
|
||||
return res, err
|
||||
}
|
||||
case params.Username != "":
|
||||
accountExists, accountDescr, err = oper.mdb.GetAccountByUsername(ctx, params.Username)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if !accountExists {
|
||||
err := fmt.Errorf("Account with name %s dont exists", params.Username)
|
||||
return res, err
|
||||
}
|
||||
default:
|
||||
err := fmt.Errorf("Empty username and accountId parameter")
|
||||
return res, err
|
||||
}
|
||||
grantExists, _, err := oper.mdb.GetGrantByAccoundIDRightPattern(ctx, params.AccountID, params.Right, params.Pattern)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if grantExists {
|
||||
err := fmt.Errorf("Grant with this right already exists")
|
||||
return res, err
|
||||
}
|
||||
now := auxtool.TimeNow()
|
||||
grantDescr := &descr.Grant{
|
||||
ID: auxuuid.NewUUID(),
|
||||
AccountID: accountDescr.ID,
|
||||
Right: params.Right,
|
||||
Pattern: params.Pattern,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
CreatedBy: operatorID,
|
||||
UpdatedBy: operatorID,
|
||||
}
|
||||
err = oper.mdb.InsertGrant(ctx, grantDescr)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
res.GrantID = grantDescr.ID
|
||||
return res, err
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package accoper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"mbase/pkg/descr"
|
||||
)
|
||||
|
||||
type DeleteAccountParams struct {
|
||||
Username string `json:"username"`
|
||||
AccountID string `json:"accountId"`
|
||||
}
|
||||
type DeleteAccountResult struct{}
|
||||
|
||||
func (oper *Operator) DeleteAccount(ctx context.Context, operatorID string, params *DeleteAccountParams) (*DeleteAccountResult, error) {
|
||||
var err error
|
||||
res := &DeleteAccountResult{}
|
||||
|
||||
if params.Username == "" && params.AccountID == "" {
|
||||
err := fmt.Errorf("Empty username and accountId parameter")
|
||||
return res, err
|
||||
}
|
||||
|
||||
var accountDescr *descr.Account
|
||||
var accountExists bool
|
||||
switch {
|
||||
case params.AccountID != "":
|
||||
accountExists, accountDescr, err = oper.mdb.GetAccountByID(ctx, params.AccountID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if !accountExists {
|
||||
err := fmt.Errorf("Account with ID %s dont exists", params.AccountID)
|
||||
return res, err
|
||||
}
|
||||
case params.Username != "":
|
||||
accountExists, accountDescr, err = oper.mdb.GetAccountByUsername(ctx, params.Username)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if !accountExists {
|
||||
err := fmt.Errorf("Account with name %s dont exists", params.Username)
|
||||
return res, err
|
||||
}
|
||||
default:
|
||||
err := fmt.Errorf("Empty username and accountId parameter")
|
||||
return res, err
|
||||
}
|
||||
if accountDescr == nil {
|
||||
err := fmt.Errorf("Null account desriptor")
|
||||
return res, err
|
||||
}
|
||||
err = oper.mdb.DeleteAllGrantsForAccountID(ctx, accountDescr.ID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
err = oper.mdb.DeleteAccountByID(ctx, accountDescr.ID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package accoper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"mbase/pkg/descr"
|
||||
)
|
||||
|
||||
// DeleteGrant
|
||||
type DeleteGrantParams struct {
|
||||
GrantID string `json:"grantId"`
|
||||
}
|
||||
type DeleteGrantResult struct{}
|
||||
|
||||
func (oper *Operator) DeleteGrant(ctx context.Context, operatorID string, params *DeleteGrantParams) (*DeleteGrantResult, error) {
|
||||
var err error
|
||||
res := &DeleteGrantResult{}
|
||||
|
||||
if params.GrantID == "" {
|
||||
err := fmt.Errorf("Empty grantId parameter")
|
||||
return res, err
|
||||
}
|
||||
|
||||
var grantDescr *descr.Grant
|
||||
var grantExists bool
|
||||
|
||||
grantExists, grantDescr, err = oper.mdb.GetGrantByID(ctx, params.GrantID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if !grantExists {
|
||||
err := fmt.Errorf("Grant with ID %s dont exists", params.GrantID)
|
||||
return res, err
|
||||
}
|
||||
err = oper.mdb.DeleteGrantByID(ctx, grantDescr.ID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package accoper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"mbase/pkg/descr"
|
||||
)
|
||||
|
||||
// GetAccount
|
||||
type GetAccountParams struct {
|
||||
Username string `json:"username"`
|
||||
AccountID string `json:"accountId"`
|
||||
}
|
||||
type GetAccountResult struct {
|
||||
Account *descr.AccountShort `json:"account"`
|
||||
}
|
||||
|
||||
func (oper *Operator) GetAccount(ctx context.Context, operatorID string, params *GetAccountParams) (*GetAccountResult, error) {
|
||||
var err error
|
||||
res := &GetAccountResult{}
|
||||
|
||||
if params.Username == "" && params.AccountID == "" {
|
||||
err := fmt.Errorf("Empty username and accountId parameter")
|
||||
return res, err
|
||||
}
|
||||
|
||||
var accountDescr *descr.Account
|
||||
var accountExists bool
|
||||
switch {
|
||||
case params.AccountID != "":
|
||||
accountExists, accountDescr, err = oper.mdb.GetAccountByID(ctx, params.AccountID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if !accountExists {
|
||||
err := fmt.Errorf("Account with ID %s dont exists", params.AccountID)
|
||||
return res, err
|
||||
}
|
||||
case params.Username != "":
|
||||
accountExists, accountDescr, err = oper.mdb.GetAccountByUsername(ctx, params.Username)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if !accountExists {
|
||||
err := fmt.Errorf("Account with name %s dont exists", params.Username)
|
||||
return res, err
|
||||
}
|
||||
default:
|
||||
err := fmt.Errorf("Empty username and accountId parameter")
|
||||
return res, err
|
||||
}
|
||||
if accountDescr == nil {
|
||||
err := fmt.Errorf("Null account desriptor")
|
||||
return res, err
|
||||
}
|
||||
accountShort := &descr.AccountShort{
|
||||
ID: accountDescr.ID,
|
||||
Username: accountDescr.Username,
|
||||
CreatedAt: accountDescr.CreatedAt,
|
||||
UpdatedAt: accountDescr.UpdatedAt,
|
||||
CreatedBy: accountDescr.CreatedBy,
|
||||
UpdatedBy: accountDescr.UpdatedBy,
|
||||
Disabled: accountDescr.Disabled,
|
||||
Grants: make([]descr.Grant, 0),
|
||||
}
|
||||
grantDescrs, err := oper.mdb.ListGrantsByAccountID(ctx, accountDescr.ID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
accountShort.Grants = grantDescrs
|
||||
|
||||
res.Account = accountShort
|
||||
return res, err
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package accoper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"mbase/pkg/descr"
|
||||
)
|
||||
|
||||
// Get Grants
|
||||
type GetGrantParams struct {
|
||||
GrantID string `json:"grantId"`
|
||||
}
|
||||
type GetGrantResult struct {
|
||||
Grant *descr.Grant `json:"grant"`
|
||||
}
|
||||
|
||||
func (oper *Operator) GetGrant(ctx context.Context, operatorID string, params *GetGrantParams) (*GetGrantResult, error) {
|
||||
var err error
|
||||
res := &GetGrantResult{}
|
||||
|
||||
if params.GrantID == "" {
|
||||
err := fmt.Errorf("Empty grantId parameter")
|
||||
return res, err
|
||||
}
|
||||
|
||||
var grantDescr *descr.Grant
|
||||
var grantExists bool
|
||||
grantExists, grantDescr, err = oper.mdb.GetGrantByID(ctx, params.GrantID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if !grantExists {
|
||||
err := fmt.Errorf("Grant with ID %s dont exists", params.GrantID)
|
||||
return res, err
|
||||
}
|
||||
|
||||
res.Grant = grantDescr
|
||||
return res, err
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package accoper
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"mbase/pkg/descr"
|
||||
)
|
||||
|
||||
type ListAccountsParams struct{}
|
||||
type ListAccountsResult struct {
|
||||
Accounts []descr.AccountShort `json:"accounts"`
|
||||
}
|
||||
|
||||
func (oper *Operator) ListAccounts(ctx context.Context, params *ListAccountsParams) (*ListAccountsResult, error) {
|
||||
var err error
|
||||
res := &ListAccountsResult{}
|
||||
|
||||
accountDescrs, err := oper.mdb.ReducedListAccounts(ctx)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
for _, accountDescr := range accountDescrs {
|
||||
accountShort := descr.AccountShort{
|
||||
ID: accountDescr.ID,
|
||||
Username: accountDescr.Username,
|
||||
Disabled: accountDescr.Disabled,
|
||||
CreatedAt: accountDescr.CreatedAt,
|
||||
UpdatedAt: accountDescr.UpdatedAt,
|
||||
CreatedBy: accountDescr.CreatedBy,
|
||||
UpdatedBy: accountDescr.UpdatedBy,
|
||||
Grants: make([]descr.Grant, 0),
|
||||
}
|
||||
grantDescrs, err := oper.mdb.ListGrantsByAccountID(ctx, accountDescr.ID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
accountShort.Grants = grantDescrs
|
||||
res.Accounts = append(res.Accounts, accountShort)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package accoper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"mbase/pkg/descr"
|
||||
)
|
||||
|
||||
// ListGrants
|
||||
type ListGrantsParams struct {
|
||||
Username string
|
||||
AccountID string
|
||||
}
|
||||
type ListGrantsResult struct {
|
||||
Grants []descr.Grant `json:"grants"`
|
||||
}
|
||||
|
||||
func (oper *Operator) ListGrants(ctx context.Context, operatorID string, params *ListGrantsParams) (*ListGrantsResult, error) {
|
||||
var err error
|
||||
res := &ListGrantsResult{
|
||||
Grants: make([]descr.Grant, 0),
|
||||
}
|
||||
var accountDescr *descr.Account
|
||||
var accountExists bool
|
||||
switch {
|
||||
case params.AccountID != "":
|
||||
accountExists, accountDescr, err = oper.mdb.GetAccountByID(ctx, params.AccountID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if !accountExists {
|
||||
err := fmt.Errorf("Account with ID %s dont exists", params.AccountID)
|
||||
return res, err
|
||||
}
|
||||
case params.Username != "":
|
||||
accountExists, accountDescr, err = oper.mdb.GetAccountByUsername(ctx, params.Username)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if !accountExists {
|
||||
err := fmt.Errorf("Account with name %s dont exists", params.Username)
|
||||
return res, err
|
||||
}
|
||||
default:
|
||||
err := fmt.Errorf("Empty username and accountId parameter")
|
||||
return res, err
|
||||
}
|
||||
accountID := accountDescr.ID
|
||||
grantDescrs, err := oper.mdb.ListGrantsByAccountID(ctx, accountID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
res.Grants = grantDescrs
|
||||
return res, err
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package accoper
|
||||
|
||||
import (
|
||||
"mbase/app/logger"
|
||||
"mbase/app/maindb"
|
||||
)
|
||||
|
||||
type OperatorParams struct {
|
||||
MainDB *maindb.Database
|
||||
}
|
||||
|
||||
type Operator struct {
|
||||
mdb *maindb.Database
|
||||
logg *logger.Logger
|
||||
}
|
||||
|
||||
func NewOperator(params *OperatorParams) (*Operator, error) {
|
||||
var err error
|
||||
oper := &Operator{
|
||||
mdb: params.MainDB,
|
||||
}
|
||||
oper.logg = logger.NewLoggerWithSubject("imageoper")
|
||||
return oper, err
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package accoper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"mbase/pkg/auxpwd"
|
||||
"mbase/pkg/auxtool"
|
||||
"mbase/pkg/descr"
|
||||
)
|
||||
|
||||
type UpdateAccountParams struct {
|
||||
Username string `json:"username"`
|
||||
AccountID string `json:"accountId"`
|
||||
NewUsername string `json:"newUsername"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
type UpdateAccountResult struct{}
|
||||
|
||||
func (oper *Operator) UpdateAccount(ctx context.Context, operatorID string, params *UpdateAccountParams) (*UpdateAccountResult, error) {
|
||||
var err error
|
||||
res := &UpdateAccountResult{}
|
||||
if params.Username == "" && params.AccountID == "" {
|
||||
err := fmt.Errorf("Empty username and accountId parameter")
|
||||
return res, err
|
||||
}
|
||||
var accountDescr *descr.Account
|
||||
var accountExists bool
|
||||
switch {
|
||||
case params.AccountID != "":
|
||||
accountExists, accountDescr, err = oper.mdb.GetAccountByID(ctx, params.AccountID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if !accountExists {
|
||||
err := fmt.Errorf("Account with ID %s dont exists", params.AccountID)
|
||||
return res, err
|
||||
}
|
||||
case params.Username != "":
|
||||
accountExists, accountDescr, err = oper.mdb.GetAccountByUsername(ctx, params.Username)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if !accountExists {
|
||||
err := fmt.Errorf("Account with name %s dont exists", params.Username)
|
||||
return res, err
|
||||
}
|
||||
default:
|
||||
err := fmt.Errorf("Empty username and accountId parameter")
|
||||
return res, err
|
||||
}
|
||||
if accountDescr == nil {
|
||||
err := fmt.Errorf("Null account desriptor")
|
||||
return res, err
|
||||
}
|
||||
now := auxtool.TimeNow()
|
||||
if params.NewUsername != "" {
|
||||
accountDescr.UpdatedAt = now
|
||||
accountDescr.Username = params.NewUsername
|
||||
}
|
||||
if params.NewPassword != "" {
|
||||
accountDescr.UpdatedAt = now
|
||||
passhash := auxpwd.MakeSHA256Hash([]byte(params.NewPassword))
|
||||
accountDescr.Passhash = passhash
|
||||
}
|
||||
if params.Disabled != accountDescr.Disabled {
|
||||
accountDescr.UpdatedAt = now
|
||||
accountDescr.Disabled = params.Disabled
|
||||
}
|
||||
|
||||
err = oper.mdb.UpdateAccountByID(ctx, accountDescr.ID, accountDescr)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package accoper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"mbase/pkg/auxtool"
|
||||
"mbase/pkg/descr"
|
||||
)
|
||||
|
||||
// UpdateGrant
|
||||
type UpdateGrantParams struct {
|
||||
GrantID string
|
||||
NewPattern string
|
||||
}
|
||||
type UpdateGrantResult struct{}
|
||||
|
||||
func (oper *Operator) UpdateGrant(ctx context.Context, operatorID string, params *UpdateGrantParams) (*UpdateGrantResult, error) {
|
||||
var err error
|
||||
res := &UpdateGrantResult{}
|
||||
|
||||
if params.NewPattern == "" {
|
||||
err := fmt.Errorf("Empty newPattern parameter")
|
||||
return res, err
|
||||
}
|
||||
if params.GrantID == "" {
|
||||
err := fmt.Errorf("Empty grantId parameter")
|
||||
return res, err
|
||||
}
|
||||
var grantDescr *descr.Grant
|
||||
var grantExists bool
|
||||
|
||||
grantExists, grantDescr, err = oper.mdb.GetGrantByID(ctx, params.GrantID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if !grantExists {
|
||||
err := fmt.Errorf("Grant with ID %s dont exists", params.GrantID)
|
||||
return res, err
|
||||
}
|
||||
now := auxtool.TimeNow()
|
||||
if params.NewPattern != "" {
|
||||
grantDescr.UpdatedAt = now
|
||||
grantDescr.UpdatedBy = operatorID
|
||||
grantDescr.Pattern = params.NewPattern
|
||||
}
|
||||
err = oper.mdb.UpdateGrantByID(ctx, grantDescr.ID, grantDescr)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"mbase/pkg/auxx509"
|
||||
|
||||
yaml "go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
Address string `json:"address" yaml:"address"`
|
||||
Port uint32 `json:"port" yaml:"port"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
Basepath string `json:"basepath" yaml:"basepath"`
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
Basepath string `json:"basepath" yaml:"basepath"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Service Service `json:"service" yaml:"service"`
|
||||
Database Database `json:"database" yaml:"database"`
|
||||
Storage Storage `json:"storage" yaml:"storage"`
|
||||
AsDaemon bool `json:"asDaemon" yaml:"asDaemon"`
|
||||
Logpath string `json:"logpath" yaml:"logpath"`
|
||||
Runpath string `json:"runpath" yaml:"runpath"`
|
||||
Version string `json:"version" yaml:"version"`
|
||||
Certpath string `json:"certpath,omitempty" yaml:"certpath,omitempty"`
|
||||
Keypath string `json:"keypath,omitempty" yaml:"keypath,omitempty"`
|
||||
X509Cert string `json:"-" yaml:"-"`
|
||||
X509Key string `json:"-" yaml:"-"`
|
||||
Datadir string `json:"datadir" yaml:datadir`
|
||||
Hostname string `json:"hostname" yaml:hostname`
|
||||
Hostnames []string `json:"hostnames" yaml:hostnames`
|
||||
LogLimit int64 `json:"logLimit" yaml:logLimit`
|
||||
RunUser string `json:"runUser" yaml:runUser`
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
logfile := fmt.Sprintf("%s.log", srvname)
|
||||
logpath := filepath.Join(logdir, logfile)
|
||||
|
||||
runfile := fmt.Sprintf("%s.pid", srvname)
|
||||
runpath := filepath.Join(rundir, runfile)
|
||||
|
||||
//certpath := fmt.Sprintf("%s.crt", srvname)
|
||||
//certpath = filepath.Join(confdir, certpath)
|
||||
|
||||
//keypath := fmt.Sprintf("%s.crt", srvname)
|
||||
//keypath = filepath.Join(confdir, keypath)
|
||||
|
||||
return &Config{
|
||||
Service: Service{
|
||||
Address: "0.0.0.0",
|
||||
Port: 1025,
|
||||
},
|
||||
Database: Database{
|
||||
Basepath: datadir,
|
||||
},
|
||||
Storage: Storage{
|
||||
Basepath: datadir,
|
||||
},
|
||||
AsDaemon: false,
|
||||
Logpath: logpath,
|
||||
Runpath: runpath,
|
||||
Version: version,
|
||||
Datadir: datadir,
|
||||
//Certpath: certpath,
|
||||
//Keypath: keypath,
|
||||
Hostnames: make([]string, 0),
|
||||
LogLimit: 1024 * 1024 * 10, // 10 Mb
|
||||
RunUser: "daemon",
|
||||
}
|
||||
}
|
||||
|
||||
func (conf *Config) String() string {
|
||||
confbytes, _ := yaml.Marshal(conf)
|
||||
return string(confbytes)
|
||||
}
|
||||
|
||||
func (conf *Config) ReadConfigfile() error {
|
||||
conffile := fmt.Sprintf("%s.yaml", srvname)
|
||||
confpath := filepath.Join(confdir, conffile)
|
||||
|
||||
confdata, err := ioutil.ReadFile(confpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = yaml.Unmarshal(confdata, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (conf *Config) ReadX509Cert() error {
|
||||
var err error
|
||||
if conf.Certpath != "" && conf.Keypath != "" {
|
||||
if !filepath.IsAbs(conf.Certpath) {
|
||||
conf.Certpath = filepath.Join(confdir, conf.Certpath)
|
||||
}
|
||||
certBytes, err := os.ReadFile(conf.Certpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !filepath.IsAbs(conf.Keypath) {
|
||||
conf.Keypath = filepath.Join(confdir, conf.Keypath)
|
||||
}
|
||||
keyBytes, err := os.ReadFile(conf.Keypath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf.X509Cert = string(certBytes)
|
||||
conf.X509Key = string(keyBytes)
|
||||
return err
|
||||
}
|
||||
/*
|
||||
if conf.X509Cert != "" && conf.X509Key != "" {
|
||||
x509Cert, err := base64.StdEncoding.DecodeString(conf.X509Cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf.X509Cert = string(x509Cert)
|
||||
x509Key, err := base64.StdEncoding.DecodeString(conf.X509Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf.X509Key = string(x509Key)
|
||||
}
|
||||
*/
|
||||
if conf.X509Cert == "" || conf.X509Key == "" {
|
||||
if conf.Hostname == "" {
|
||||
conf.Hostname, err = os.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
certBytes, keyBytes, err := auxx509.CreateSelfSignedCert(conf.Hostname, conf.Hostnames...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf.X509Cert = string(certBytes)
|
||||
conf.X509Key = string(keyBytes)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package config
|
||||
|
||||
const (
|
||||
confdir = "@srv_confdir@"
|
||||
rundir = "@srv_rundir@"
|
||||
logdir = "@srv_logdir@"
|
||||
datadir = "@srv_datadir@"
|
||||
version = "@PACKAGE_VERSION@"
|
||||
srvname = "@PACKAGE_NAME@d"
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"mbase/app/router"
|
||||
"mbase/pkg/auxhttp"
|
||||
"mbase/pkg/auxpwd"
|
||||
"mbase/pkg/terms"
|
||||
)
|
||||
|
||||
const (
|
||||
authTag = "authpass"
|
||||
userTag = "accountID"
|
||||
)
|
||||
|
||||
func (hand *Handler) AuthMiddleware(next router.Handler) router.Handler {
|
||||
var handlerFunc router.HandlerFunc
|
||||
|
||||
handlerFunc = func(rctx *router.Context) {
|
||||
success, accountID, err := hand.CheckAccess(rctx)
|
||||
if success {
|
||||
rctx.SetBool(authTag, true)
|
||||
rctx.SetString(userTag, string(accountID))
|
||||
}
|
||||
if err != nil {
|
||||
hand.logg.Errorf("Authorization middleware error: %v", err)
|
||||
}
|
||||
next.ServeHTTP(rctx)
|
||||
}
|
||||
return handlerFunc
|
||||
}
|
||||
|
||||
// Authentification
|
||||
func (hand *Handler) CheckAccess(rctx *router.Context) (bool, string, error) {
|
||||
var err error
|
||||
var success bool
|
||||
var username string
|
||||
var password string
|
||||
var accountID string
|
||||
|
||||
accountID = terms.AnonymousID
|
||||
|
||||
//hand.logg.Debugf("URL: %s", rctx.URL().String())
|
||||
authHeader := rctx.GetHeader("Authorization")
|
||||
hand.logg.Debugf("Authorization: [%s]", authHeader)
|
||||
if authHeader != "" {
|
||||
username, password, err = auxhttp.ParseBasicAuth(authHeader)
|
||||
if err != nil {
|
||||
return success, accountID, err
|
||||
}
|
||||
if username == "" || password == "" {
|
||||
goto anonymous
|
||||
}
|
||||
|
||||
success, id, err := hand.ValidatePassword(rctx.Ctx, username, password)
|
||||
if err != nil {
|
||||
return false, accountID, err
|
||||
}
|
||||
if !success {
|
||||
err = fmt.Errorf("Incorrect username or password")
|
||||
return false, accountID, err
|
||||
}
|
||||
accountID = id
|
||||
return success, accountID, err
|
||||
}
|
||||
anonymous:
|
||||
success = true
|
||||
accountID = terms.AnonymousID
|
||||
return success, accountID, err
|
||||
}
|
||||
|
||||
func (hand *Handler) ValidatePassword(ctx context.Context, username, password string) (bool, string, error) {
|
||||
var err error
|
||||
var accountID string
|
||||
valid := false
|
||||
|
||||
accountExists, accountDescr, err := hand.mdb.GetAccountByUsername(ctx, username)
|
||||
if !accountExists {
|
||||
err := fmt.Errorf("Account not exists")
|
||||
return valid, accountID, err
|
||||
}
|
||||
if !auxpwd.PasswordMatch([]byte(password), accountDescr.Passhash) {
|
||||
err := fmt.Errorf("Login data mismatch")
|
||||
return valid, accountID, err
|
||||
}
|
||||
valid = true
|
||||
accountID = accountDescr.ID
|
||||
|
||||
return valid, accountID, err
|
||||
}
|
||||
|
||||
// Authorization
|
||||
func (hand *Handler) CheckRight(ctx context.Context, accountID, reqRight, subject string) (bool, error) {
|
||||
var err error
|
||||
var res bool
|
||||
res = true
|
||||
return res, err
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"mbase/app/accoper"
|
||||
"mbase/app/router"
|
||||
"mbase/pkg/terms"
|
||||
)
|
||||
|
||||
// POST /v3/account/create 200 200
|
||||
func (hand *Handler) CreateAccount(rctx *router.Context) {
|
||||
var err error
|
||||
|
||||
params := &accoper.CreateAccountParams{}
|
||||
err = rctx.BindJSON(params)
|
||||
if err != nil {
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Rigth checking
|
||||
operatorID, _ := rctx.GetString(userTag)
|
||||
opEnable, err := hand.CheckRight(rctx.Ctx, operatorID, terms.RightWriteAccounts, "")
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Operation error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
if !opEnable {
|
||||
err := fmt.Errorf("Operation not enabled for this account")
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Execution of the operation
|
||||
res, err := hand.acop.CreateAccount(rctx.Ctx, operatorID, params)
|
||||
if err != nil {
|
||||
hand.logg.Errorf("Operation error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
hand.SendResult(rctx, res)
|
||||
}
|
||||
|
||||
// POST /v3/account/get 200 200
|
||||
func (hand *Handler) GetAccount(rctx *router.Context) {
|
||||
var err error
|
||||
|
||||
params := &accoper.GetAccountParams{}
|
||||
err = rctx.BindJSON(params)
|
||||
if err != nil {
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Rigth checking
|
||||
operatorID, _ := rctx.GetString(userTag)
|
||||
opEnable, err := hand.CheckRight(rctx.Ctx, operatorID, terms.RightWriteAccounts, "")
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Operation error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
if !opEnable {
|
||||
err := fmt.Errorf("Operation not enabled for this account")
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Execution of the operation
|
||||
res, err := hand.acop.GetAccount(rctx.Ctx, operatorID, params)
|
||||
if err != nil {
|
||||
hand.logg.Errorf("Operation error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
hand.SendResult(rctx, res)
|
||||
}
|
||||
|
||||
// POST /v3/accounts/list 200 200
|
||||
func (hand *Handler) ListAccounts(rctx *router.Context) {
|
||||
var err error
|
||||
|
||||
params := &accoper.ListAccountsParams{}
|
||||
err = rctx.BindJSON(params)
|
||||
if err != nil {
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Rigth checking
|
||||
operatorID, _ := rctx.GetString(userTag)
|
||||
opEnable, err := hand.CheckRight(rctx.Ctx, operatorID, terms.RightWriteAccounts, "")
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Operation error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
if !opEnable {
|
||||
err := fmt.Errorf("Operation not enabled for this account")
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Execution of the operation
|
||||
res, err := hand.acop.ListAccounts(rctx.Ctx, params)
|
||||
if err != nil {
|
||||
hand.logg.Errorf("ListAccounts error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
hand.SendResult(rctx, res)
|
||||
}
|
||||
|
||||
// POST /v3/account/get 200 200
|
||||
func (hand *Handler) UpdateAccount(rctx *router.Context) {
|
||||
var err error
|
||||
|
||||
params := &accoper.UpdateAccountParams{}
|
||||
err = rctx.BindJSON(params)
|
||||
if err != nil {
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Rigth checking
|
||||
operatorID, _ := rctx.GetString(userTag)
|
||||
opEnable, err := hand.CheckRight(rctx.Ctx, operatorID, terms.RightWriteAccounts, "")
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Operation error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
if !opEnable {
|
||||
err := fmt.Errorf("Operation not enabled for this account")
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Execution of the operation
|
||||
res, err := hand.acop.UpdateAccount(rctx.Ctx, operatorID, params)
|
||||
if err != nil {
|
||||
hand.logg.Errorf("UpdateAccount error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
hand.SendResult(rctx, res)
|
||||
}
|
||||
|
||||
// POST /v3/account/delete 200 200
|
||||
func (hand *Handler) DeleteAccount(rctx *router.Context) {
|
||||
var err error
|
||||
|
||||
params := &accoper.DeleteAccountParams{}
|
||||
err = rctx.BindJSON(params)
|
||||
if err != nil {
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Rigth checking
|
||||
operatorID, _ := rctx.GetString(userTag)
|
||||
opEnable, err := hand.CheckRight(rctx.Ctx, operatorID, terms.RightWriteAccounts, params.Username)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Operation error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
if !opEnable {
|
||||
err := fmt.Errorf("Operation not enabled for this account")
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Execution of the operation
|
||||
res, err := hand.acop.DeleteAccount(rctx.Ctx, operatorID, params)
|
||||
if err != nil {
|
||||
hand.logg.Errorf("DeleteAccount error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
hand.SendResult(rctx, res)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"mbase/app/accoper"
|
||||
"mbase/app/router"
|
||||
"mbase/pkg/terms"
|
||||
)
|
||||
|
||||
// POST /v3/grant/create 200 200
|
||||
func (hand *Handler) CreateGrant(rctx *router.Context) {
|
||||
var err error
|
||||
|
||||
params := &accoper.CreateGrantParams{}
|
||||
err = rctx.BindJSON(params)
|
||||
if err != nil {
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Rigth checking
|
||||
operatorID, _ := rctx.GetString(userTag)
|
||||
opEnable, err := hand.CheckRight(rctx.Ctx, operatorID, terms.RightWriteAccounts, "")
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Operation error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
if !opEnable {
|
||||
err := fmt.Errorf("Operation not enabled for this account")
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Execution of the operation
|
||||
res, err := hand.acop.CreateGrant(rctx.Ctx, operatorID, params)
|
||||
if err != nil {
|
||||
hand.logg.Errorf("CreateGrant error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
hand.SendResult(rctx, res)
|
||||
}
|
||||
|
||||
// POST /v3/grant/get 200 200
|
||||
func (hand *Handler) GetGrant(rctx *router.Context) {
|
||||
var err error
|
||||
|
||||
params := &accoper.GetGrantParams{}
|
||||
err = rctx.BindJSON(params)
|
||||
if err != nil {
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Rigth checking
|
||||
operatorID, _ := rctx.GetString(userTag)
|
||||
opEnable, err := hand.CheckRight(rctx.Ctx, operatorID, terms.RightReadAccounts, "")
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Operation error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
if !opEnable {
|
||||
err := fmt.Errorf("Operation not enabled for this account")
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Execution of the operation
|
||||
res, err := hand.acop.GetGrant(rctx.Ctx, operatorID, params)
|
||||
if err != nil {
|
||||
hand.logg.Errorf("CreateGrant error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
hand.SendResult(rctx, res)
|
||||
}
|
||||
|
||||
// POST /v3/grants/list 200 200
|
||||
func (hand *Handler) ListGrants(rctx *router.Context) {
|
||||
var err error
|
||||
|
||||
params := &accoper.ListGrantsParams{}
|
||||
err = rctx.BindJSON(params)
|
||||
if err != nil {
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Rigth checking
|
||||
operatorID, _ := rctx.GetString(userTag)
|
||||
opEnable, err := hand.CheckRight(rctx.Ctx, operatorID, terms.RightReadAccounts, "")
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Operation error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
if !opEnable {
|
||||
err := fmt.Errorf("Operation not enabled for this account")
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Execution of the operation
|
||||
res, err := hand.acop.ListGrants(rctx.Ctx, operatorID, params)
|
||||
if err != nil {
|
||||
hand.logg.Errorf("ListGrants error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
hand.SendResult(rctx, res)
|
||||
}
|
||||
|
||||
// POST /v3/grant/get 200 200
|
||||
func (hand *Handler) UpdateGrant(rctx *router.Context) {
|
||||
var err error
|
||||
|
||||
params := &accoper.UpdateGrantParams{}
|
||||
err = rctx.BindJSON(params)
|
||||
if err != nil {
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Rigth checking
|
||||
operatorID, _ := rctx.GetString(userTag)
|
||||
opEnable, err := hand.CheckRight(rctx.Ctx, operatorID, terms.RightWriteAccounts, "")
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Operation error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
if !opEnable {
|
||||
err := fmt.Errorf("Operation not enabled for this account")
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Execution of the operation
|
||||
res, err := hand.acop.UpdateGrant(rctx.Ctx, operatorID, params)
|
||||
if err != nil {
|
||||
hand.logg.Errorf("UpdateGrant error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
hand.SendResult(rctx, res)
|
||||
}
|
||||
|
||||
// POST /v3/grant/delete 200 200
|
||||
func (hand *Handler) DeleteGrant(rctx *router.Context) {
|
||||
var err error
|
||||
|
||||
params := &accoper.DeleteGrantParams{}
|
||||
err = rctx.BindJSON(params)
|
||||
if err != nil {
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Rigth checking
|
||||
operatorID, _ := rctx.GetString(userTag)
|
||||
opEnable, err := hand.CheckRight(rctx.Ctx, operatorID, terms.RightWriteAccounts, "")
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Operation error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
if !opEnable {
|
||||
err := fmt.Errorf("Operation not enabled for this account")
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
// Execution of the operation
|
||||
res, err := hand.acop.DeleteGrant(rctx.Ctx, operatorID, params)
|
||||
if err != nil {
|
||||
hand.logg.Errorf("DeleteGrant error: %v", err)
|
||||
hand.SendError(rctx, err)
|
||||
return
|
||||
}
|
||||
hand.SendResult(rctx, res)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package handler
|
||||
|
||||
import (
|
||||
"mbase/app/logger"
|
||||
"mbase/app/maindb"
|
||||
"mbase/app/router"
|
||||
|
||||
"mbase/app/accoper"
|
||||
"mbase/app/servoper"
|
||||
|
||||
yaml "go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
type HandlerParams struct {
|
||||
MainDB *maindb.Database
|
||||
AccOper *accoper.Operator
|
||||
ServOper *servoper.Operator
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
mdb *maindb.Database
|
||||
logg *logger.Logger
|
||||
acop *accoper.Operator
|
||||
seop *servoper.Operator
|
||||
}
|
||||
|
||||
func NewHandler(params *HandlerParams) (*Handler, error) {
|
||||
var err error
|
||||
hand := &Handler{
|
||||
mdb: params.MainDB,
|
||||
acop: params.AccOper,
|
||||
seop: params.ServOper,
|
||||
}
|
||||
hand.logg = logger.NewLoggerWithSubject("handler")
|
||||
return hand, err
|
||||
}
|
||||
|
||||
func (hand *Handler) DumpHeaders(label string, rctx *router.Context) {
|
||||
headers := rctx.GetHeaders()
|
||||
yamlData, _ := yaml.Marshal(headers)
|
||||
hand.logg.Debugf("%s:\n%s\n", label, string(yamlData))
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"mbase/app/router"
|
||||
)
|
||||
|
||||
func (hand *Handler) NotFound(rctx *router.Context) {
|
||||
hand.logg.Warningf("Route for [%s %s] not found", rctx.Request.Method, rctx.Request.URL.String())
|
||||
rctx.SetStatus(http.StatusNotFound)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"mbase/app/router"
|
||||
)
|
||||
|
||||
type Response[T any] struct {
|
||||
Error bool `json:"error" yaml:"error"`
|
||||
Message string `json:"message,omitempty" yaml:"message,omitempty"`
|
||||
Result T `json:"result,omitempty" yaml:"result,result"`
|
||||
}
|
||||
|
||||
func NewResponse[T any]() *Response[T] {
|
||||
return &Response[T]{}
|
||||
}
|
||||
|
||||
func (hand *Handler) SendResult(rctx *router.Context, result any) {
|
||||
response := &Response[any]{
|
||||
Error: false,
|
||||
Result: result,
|
||||
}
|
||||
rctx.SendJSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (hand *Handler) SendError(rctx *router.Context, err error) {
|
||||
response := &Response[any]{
|
||||
Error: true,
|
||||
Message: err.Error(),
|
||||
}
|
||||
rctx.SendJSON(http.StatusOK, response)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package handler
|
||||
|
||||
import (
|
||||
"mbase/app/servoper"
|
||||
"mbase/app/router"
|
||||
)
|
||||
|
||||
func (hand *Handler) SendHello(rctx *router.Context) {
|
||||
params := &servoper.SendHelloParams{}
|
||||
res, _ := hand.seop.SendHello(params)
|
||||
hand.SendResult(rctx, res)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package locker
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Elem struct {
|
||||
Pipe chan bool
|
||||
Usage int
|
||||
}
|
||||
|
||||
func NewElem() *Elem {
|
||||
return &Elem{
|
||||
Pipe: make(chan bool, 1),
|
||||
}
|
||||
}
|
||||
|
||||
type Locker struct {
|
||||
mtx sync.Mutex
|
||||
lMap map[string]*Elem
|
||||
}
|
||||
|
||||
func NewLocker() *Locker {
|
||||
lock := &Locker{
|
||||
lMap: make(map[string]*Elem),
|
||||
}
|
||||
return lock
|
||||
}
|
||||
|
||||
func (lock *Locker) WaitAndLock(name string) {
|
||||
lock.mtx.Lock()
|
||||
p, exist := lock.lMap[name]
|
||||
if !exist {
|
||||
p = NewElem()
|
||||
lock.lMap[name] = p
|
||||
p.Pipe <- true
|
||||
}
|
||||
p.Usage += 1
|
||||
lock.mtx.Unlock()
|
||||
select {
|
||||
case <-p.Pipe:
|
||||
// NOP
|
||||
}
|
||||
}
|
||||
|
||||
func (lock *Locker) Done(name string) {
|
||||
lock.mtx.Lock()
|
||||
p, exist := lock.lMap[name]
|
||||
if exist {
|
||||
p.Pipe <- true
|
||||
if p.Usage > 0 {
|
||||
p.Usage -= 1
|
||||
}
|
||||
}
|
||||
garbageKeys := make([]string, 0)
|
||||
for key, _ := range lock.lMap {
|
||||
elem := lock.lMap[key]
|
||||
if elem.Usage == 0 && key != name {
|
||||
garbageKeys = append(garbageKeys, key)
|
||||
}
|
||||
}
|
||||
for _, key := range garbageKeys {
|
||||
delete(lock.lMap, key)
|
||||
}
|
||||
lock.mtx.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package locker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Runner struct {
|
||||
lock *Locker
|
||||
res sync.Map
|
||||
}
|
||||
|
||||
func NewRunner() *Runner {
|
||||
return &Runner{
|
||||
lock: NewLocker(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) Run(wg *sync.WaitGroup, resName string, t *testing.T) {
|
||||
for n := 2; n < 1000; n++ {
|
||||
r.lock.WaitAndLock(resName)
|
||||
|
||||
val := fmt.Sprintf("%d", n)
|
||||
r.res.Store(resName, val)
|
||||
|
||||
td := time.Duration(rand.Uint64()%1000 + 1)
|
||||
time.Sleep(td * time.Nanosecond)
|
||||
|
||||
foo, exist := r.res.Load(resName)
|
||||
if !exist {
|
||||
t.Errorf("not exist!\n")
|
||||
}
|
||||
if foo != val {
|
||||
t.Errorf("not val!\n")
|
||||
}
|
||||
r.res.Delete(resName)
|
||||
r.lock.Done(resName)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
func TestLocker(t *testing.T) {
|
||||
run := NewRunner()
|
||||
var wg sync.WaitGroup
|
||||
for n := 1; n < 200; n++ {
|
||||
go run.Run(&wg, "foo", t)
|
||||
wg.Add(1)
|
||||
}
|
||||
for n := 1; n < 200; n++ {
|
||||
go run.Run(&wg, "foo/bare", t)
|
||||
wg.Add(1)
|
||||
}
|
||||
for n := 1; n < 200; n++ {
|
||||
go run.Run(&wg, "foo/bare/foo", t)
|
||||
wg.Add(1)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
mtx sync.Mutex
|
||||
output io.WriteCloser = os.Stderr
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
subject string
|
||||
writer io.WriteCloser
|
||||
mtx *sync.Mutex
|
||||
}
|
||||
|
||||
func NewLoggerWithSubject(subj string) *Logger {
|
||||
return &Logger{
|
||||
subject: subj,
|
||||
writer: output,
|
||||
mtx: &mtx,
|
||||
}
|
||||
}
|
||||
|
||||
func NewLogger() *Logger {
|
||||
return &Logger{
|
||||
writer: output,
|
||||
mtx: &mtx,
|
||||
}
|
||||
}
|
||||
|
||||
func SetWriter(newOut io.WriteCloser) {
|
||||
mtx.Lock()
|
||||
output = newOut
|
||||
mtx.Unlock()
|
||||
}
|
||||
|
||||
func (logg *Logger) SetWriter(newOut io.WriteCloser) {
|
||||
mtx.Lock()
|
||||
logg.writer = newOut
|
||||
var newMtx sync.Mutex
|
||||
logg.mtx = &newMtx
|
||||
mtx.Unlock()
|
||||
}
|
||||
|
||||
func (logg *Logger) Debugf(message string, args ...any) {
|
||||
logg.printf("debug", message, args...)
|
||||
}
|
||||
|
||||
func (logg *Logger) Infof(message string, args ...any) {
|
||||
logg.printf("info", message, args...)
|
||||
}
|
||||
|
||||
func (logg *Logger) Warningf(message string, args ...any) {
|
||||
logg.printf("warning", message, args...)
|
||||
}
|
||||
|
||||
func (logg *Logger) Errorf(message string, args ...any) {
|
||||
logg.printf("error", message, args...)
|
||||
}
|
||||
|
||||
func (logg *Logger) printf(level, message string, args ...any) {
|
||||
timestamp := time.Now().Format(time.RFC3339)
|
||||
buffer := bytes.NewBuffer([]byte{})
|
||||
if logg.subject != "" {
|
||||
fmt.Fprintf(buffer, "%s %s.%s: ", timestamp, logg.subject, level)
|
||||
} else {
|
||||
fmt.Fprintf(buffer, "%s %s: ", timestamp, level)
|
||||
}
|
||||
fmt.Fprintf(buffer, message, args...)
|
||||
fmt.Fprintf(buffer, "\n")
|
||||
logg.mtx.Lock()
|
||||
fmt.Fprint(output, buffer.String())
|
||||
logg.mtx.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package logger
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogger(t *testing.T) {
|
||||
logg := NewLogger("test")
|
||||
logg.Debugf("foo: %s", "bar")
|
||||
}
|
||||
|
||||
func BenchmarkLoggerL(b *testing.B) {
|
||||
SetWriter(ioutil.Discard)
|
||||
logg := NewLogger("test")
|
||||
for i := 0; i < b.N; i++ {
|
||||
logg.Debugf("foo: %s", "bar")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLoggerP(b *testing.B) {
|
||||
SetWriter(ioutil.Discard)
|
||||
logg := NewLogger("test")
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
logg.Debugf("foo: %s", "bar")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package maindb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"mbase/pkg/descr"
|
||||
)
|
||||
|
||||
func (db *Database) InsertAccount(ctx context.Context, account *descr.Account) error {
|
||||
var err error
|
||||
|
||||
request := `INSERT INTO accounts(id, username, passhash, disabled, created_at, updated_at, created_by, updated_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`
|
||||
_, err = db.db.Exec(request, account.ID, account.Username, account.Passhash, account.Disabled,
|
||||
account.CreatedAt, account.UpdatedAt, account.CreatedBy, account.UpdatedBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) UpdateAccountByID(ctx context.Context, accountID string, account *descr.Account) error {
|
||||
var err error
|
||||
request := `UPDATE accounts SET username = $1, passhash = $2, disabled = $3, updated_at = $4, updated_by = $5 WHERE id = $6`
|
||||
_, err = db.db.Exec(request, account.Username, account.Passhash, account.Disabled, account.UpdatedAt, account.UpdatedBy, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) ReducedListAccounts(ctx context.Context) ([]descr.Account, error) {
|
||||
var err error
|
||||
request := `SELECT id, username, disabled, created_at, updated_at, created_by, updated_by FROM accounts`
|
||||
res := make([]descr.Account, 0)
|
||||
err = db.db.Select(&res, request)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (db *Database) ListAccounts(ctx context.Context) ([]descr.Account, error) {
|
||||
var err error
|
||||
request := `SELECT * FROM accounts`
|
||||
res := make([]descr.Account, 0)
|
||||
err = db.db.Select(&res, request)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (db *Database) GetAccountByID(ctx context.Context, accountID string) (bool, *descr.Account, error) {
|
||||
var err error
|
||||
var res *descr.Account
|
||||
var exists bool = false
|
||||
|
||||
request := `SELECT * FROM accounts WHERE id = $1 LiMIT 1`
|
||||
dbRes := make([]descr.Account, 0)
|
||||
err = db.db.Select(&dbRes, request, accountID)
|
||||
if err != nil {
|
||||
return exists, res, err
|
||||
}
|
||||
if len(dbRes) == 0 {
|
||||
return exists, res, err
|
||||
}
|
||||
exists = true
|
||||
res = &dbRes[0]
|
||||
return exists, res, err
|
||||
}
|
||||
|
||||
func (db *Database) GetAccountByUsername(ctx context.Context, username string) (bool, *descr.Account, error) {
|
||||
var err error
|
||||
var res *descr.Account
|
||||
var exists bool
|
||||
|
||||
request := `SELECT * FROM accounts WHERE username = $1 LIMIT 1`
|
||||
dbRes := make([]descr.Account, 0)
|
||||
err = db.db.Select(&dbRes, request, username)
|
||||
if err != nil {
|
||||
return exists, res, err
|
||||
}
|
||||
|
||||
if len(dbRes) == 0 {
|
||||
return false, res, err
|
||||
}
|
||||
exists = true
|
||||
res = &dbRes[0]
|
||||
return exists, res, err
|
||||
}
|
||||
|
||||
func (db *Database) DeleteAccountByID(ctx context.Context, accountID string) error {
|
||||
var err error
|
||||
|
||||
request := `DELETE FROM accounts WHERE id = $1`
|
||||
_, err = db.db.Exec(request, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) DeleteAccountByUsername(ctx context.Context, username string) error {
|
||||
var err error
|
||||
|
||||
request := `DELETE FROM accounts WHERE username = $1`
|
||||
_, err = db.db.Exec(request, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package maindb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"mbase/pkg/descr"
|
||||
)
|
||||
|
||||
func (db *Database) InsertGrant(ctx context.Context, grant *descr.Grant) error {
|
||||
var err error
|
||||
request := `INSERT INTO grants(id, account_id, right, pattern, created_at, updated_at, created_by, updated_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`
|
||||
_, err = db.db.Exec(request, grant.ID, grant.AccountID, grant.Right, grant.Pattern,
|
||||
grant.CreatedAt, grant.UpdatedAt, grant.CreatedBy, grant.UpdatedBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) UpdateGrantByID(ctx context.Context, grantID string, grant *descr.Grant) error {
|
||||
var err error
|
||||
request := `UPDATE grants SET pattern = $1, updated_at = $2, updated_by = $3 WHERE id = $4`
|
||||
_, err = db.db.Exec(request, grant.Pattern, grant.UpdatedAt, grant.UpdatedBy, grantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) ListGrantsByAccountID(ctx context.Context, accountID string) ([]descr.Grant, error) {
|
||||
var err error
|
||||
request := `SELECT * FROM grants WHERE account_id = $1`
|
||||
res := make([]descr.Grant, 0)
|
||||
err = db.db.Select(&res, request, accountID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (db *Database) ListGrants(ctx context.Context) ([]descr.Grant, error) {
|
||||
var err error
|
||||
request := `SELECT * FROM grants`
|
||||
res := make([]descr.Grant, 0)
|
||||
err = db.db.Select(&res, request)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (db *Database) GetGrantByID(ctx context.Context, garntID string) (bool, *descr.Grant, error) {
|
||||
var err error
|
||||
res := &descr.Grant{}
|
||||
request := `SELECT * FROM grants WHERE id = $1 LIMIT 1`
|
||||
dbRes := make([]descr.Grant, 0)
|
||||
err = db.db.Select(&dbRes, request, garntID)
|
||||
if err != nil {
|
||||
return false, res, err
|
||||
}
|
||||
if len(dbRes) == 0 {
|
||||
return false, res, err
|
||||
|
||||
}
|
||||
res = &dbRes[0]
|
||||
return true, res, err
|
||||
}
|
||||
|
||||
func (db *Database) GetGrantByAccoundIDRight(ctx context.Context, accountID, right string) (bool, *descr.Grant, error) {
|
||||
var err error
|
||||
res := &descr.Grant{}
|
||||
request := `SELECT * FROM grants WHERE account_id = $1 AND right = $2 LIMIT 1`
|
||||
dbRes := make([]descr.Grant, 0)
|
||||
err = db.db.Select(&dbRes, request, accountID, right)
|
||||
if err != nil {
|
||||
return false, res, err
|
||||
}
|
||||
if len(dbRes) == 0 {
|
||||
return false, res, err
|
||||
|
||||
}
|
||||
res = &dbRes[0]
|
||||
return true, res, err
|
||||
}
|
||||
|
||||
func (db *Database) ListGrantsByAccoundIDRight(ctx context.Context, accountID, right string) (bool, []descr.Grant, error) {
|
||||
var err error
|
||||
request := `SELECT * FROM grants WHERE account_id = $1 AND right = $2`
|
||||
res := make([]descr.Grant, 0)
|
||||
err = db.db.Select(&res, request, accountID, right)
|
||||
if err != nil {
|
||||
return false, res, err
|
||||
}
|
||||
if len(res) == 0 {
|
||||
return false, res, err
|
||||
}
|
||||
return true, res, err
|
||||
}
|
||||
|
||||
func (db *Database) GetGrantByAccoundIDRightPattern(ctx context.Context, accountID, right, pattern string) (bool, *descr.Grant, error) {
|
||||
var err error
|
||||
res := &descr.Grant{}
|
||||
request := `SELECT * FROM grants WHERE account_id = $1 AND right = $2 AND pattern = $3 LIMIT 1`
|
||||
dbRes := make([]descr.Grant, 0)
|
||||
err = db.db.Select(&dbRes, request, accountID, right, pattern)
|
||||
if err != nil {
|
||||
return false, res, err
|
||||
}
|
||||
if len(dbRes) == 0 {
|
||||
return false, res, err
|
||||
|
||||
}
|
||||
res = &dbRes[0]
|
||||
return true, res, err
|
||||
}
|
||||
|
||||
func (db *Database) DeleteGrantByAccountIDRightPattern(ctx context.Context, accountID, right, pattern string) error {
|
||||
var err error
|
||||
request := `DELETE FROM grants WHERE account_id = $1 AND right = $2 AND pattern = $3`
|
||||
_, err = db.db.Exec(request, accountID, right, pattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) DeleteGrantByID(ctx context.Context, grantID string) error {
|
||||
var err error
|
||||
request := `DELETE FROM grants WHERE id = $1`
|
||||
_, err = db.db.Exec(request, grantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) DeleteAllGrantsForAccountID(ctx context.Context, grantID string) error {
|
||||
var err error
|
||||
request := `DELETE FROM grants WHERE account_id = $1`
|
||||
_, err = db.db.Exec(request, grantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package maindb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"mbase/pkg/auxtool"
|
||||
"mbase/pkg/auxuuid"
|
||||
"mbase/pkg/descr"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGrant(t *testing.T) {
|
||||
var err error
|
||||
|
||||
dbDir := t.TempDir()
|
||||
db := NewDatabase(dbDir)
|
||||
|
||||
err = db.OpenDatabase()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = db.InitDatabase()
|
||||
require.NoError(t, err)
|
||||
|
||||
id := auxuuid.NewUUID()
|
||||
accountID := auxuuid.NewUUID()
|
||||
timenow := auxtool.TimeNow()
|
||||
creator := auxuuid.NewUUID()
|
||||
newGrant := &descr.Grant{
|
||||
ID: id,
|
||||
AccountID: accountID,
|
||||
Right: "rigthFoo",
|
||||
Pattern: `*`,
|
||||
CreatedAt: timenow,
|
||||
UpdatedAt: timenow,
|
||||
CreatedBy: creator,
|
||||
UpdatedBy: creator,
|
||||
}
|
||||
ctx, _ := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
|
||||
err = db.InsertGrant(ctx, newGrant)
|
||||
require.NoError(t, err)
|
||||
|
||||
files, err := db.ListGrantsByAccountID(ctx, accountID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(files), 1)
|
||||
require.Equal(t, files[0].ID, id)
|
||||
require.Equal(t, files[0].AccountID, accountID)
|
||||
require.Equal(t, files[0].CreatedBy, creator)
|
||||
fmt.Println(files[0].CreatedBy)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package maindb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"mbase/pkg/auxpwd"
|
||||
"mbase/pkg/auxtool"
|
||||
"mbase/pkg/auxuuid"
|
||||
"mbase/pkg/descr"
|
||||
"mbase/pkg/terms"
|
||||
)
|
||||
|
||||
func (db *Database) WriteAnonymous(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
now := auxtool.TimeNow()
|
||||
password := auxtool.RandomString(64)
|
||||
passhash := auxpwd.MakeSHA256Hash([]byte(password))
|
||||
accountDescr := &descr.Account{
|
||||
ID: terms.AnonymousID,
|
||||
Username: terms.AnonimousUsername,
|
||||
Passhash: passhash,
|
||||
Disabled: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
CreatedBy: terms.ServerID,
|
||||
UpdatedBy: terms.ServerID,
|
||||
}
|
||||
err = db.InsertAccount(ctx, accountDescr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func (db *Database) WriteInituser(ctx context.Context) error {
|
||||
var err error
|
||||
now := auxtool.TimeNow()
|
||||
passhash := auxpwd.MakeSHA256Hash([]byte(terms.InitUsername))
|
||||
accountDescr := &descr.Account{
|
||||
ID: terms.InitID,
|
||||
Username: terms.InitUsername,
|
||||
Passhash: passhash,
|
||||
Disabled: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
CreatedBy: terms.ServerID,
|
||||
UpdatedBy: terms.ServerID,
|
||||
}
|
||||
err = db.InsertAccount(ctx, accountDescr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fullRights := []string{
|
||||
terms.RightWriteAccounts,
|
||||
terms.RightReadAccounts,
|
||||
}
|
||||
for _, right := range fullRights {
|
||||
grantDescr := &descr.Grant{
|
||||
ID: auxuuid.NewUUID(),
|
||||
AccountID: accountDescr.ID,
|
||||
Right: right,
|
||||
Pattern: ".*",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
CreatedBy: terms.ServerID,
|
||||
UpdatedBy: terms.ServerID,
|
||||
}
|
||||
err = db.InsertGrant(ctx, grantDescr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package maindb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"mbase/app/logger"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
datapath string
|
||||
logg *logger.Logger
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewDatabase(datapath string) *Database {
|
||||
return &Database{
|
||||
datapath: datapath,
|
||||
logg: logger.NewLoggerWithSubject("maindb"),
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Database) OpenDatabase() error {
|
||||
var err error
|
||||
dbPath := filepath.Join(db.datapath, "mbase.db")
|
||||
db.db, err = sqlx.Open("sqlite3", fmt.Sprintf("%s?cache=shared&mode=rwc&_journal_mode=WAL", dbPath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Open database error: %v", err)
|
||||
}
|
||||
err = db.db.Ping()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Ping database error: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) InitDatabase() error {
|
||||
var err error
|
||||
_, err = db.db.Exec(schema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Init database error: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package maindb
|
||||
|
||||
const schema = `
|
||||
--- DROP TABLE IF EXISTS accounts;
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
passhash TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
created_by TEXT NOT NULL,
|
||||
updated_by TEXT NOT NULL,
|
||||
disabled BOOL
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS accounts_index01
|
||||
ON accounts(id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS accounts_index02
|
||||
ON accounts(username);
|
||||
|
||||
--- DROP TABLE IF EXISTS grants;
|
||||
CREATE TABLE IF NOT EXISTS grants (
|
||||
id TEXT NOT NULL,
|
||||
account_id INT NOT NULL,
|
||||
right TEXT NOT NULL,
|
||||
pattern TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
created_by TEXT NOT NULL,
|
||||
updated_by TEXT NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS grants_index00
|
||||
ON grants(id);
|
||||
CREATE INDEX IF NOT EXISTS grants_index01
|
||||
ON grants(account_id);
|
||||
CREATE INDEX IF NOT EXISTS grants_index02
|
||||
ON grants(account_id, right);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS grants_index03
|
||||
ON grants(account_id, right, pattern);
|
||||
|
||||
`
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package router
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// The code reflect string-string map to taged structure
|
||||
// Limited, used only base types
|
||||
// Don't ask me how it works. I'm only writer ;)
|
||||
|
||||
func bindObj(obj interface{}, kvmap map[string]string, sTag string) error {
|
||||
var err error
|
||||
vElem := reflect.ValueOf(obj).Elem()
|
||||
sElem := reflect.TypeOf(obj).Elem()
|
||||
for i := 0; i < vElem.NumField(); i++ {
|
||||
vField := vElem.Field(i)
|
||||
tag := sElem.Field(i).Tag.Get(sTag)
|
||||
if tag != "" {
|
||||
sVal, exists := kvmap[tag]
|
||||
if exists {
|
||||
switch vField.Kind() {
|
||||
case reflect.String:
|
||||
vField.SetString(sVal)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
iVal, err := strconv.ParseInt(sVal, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vField.SetInt(iVal)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
iVal, err := strconv.ParseUint(sVal, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vField.SetUint(iVal)
|
||||
case reflect.Bool:
|
||||
bVal, err := strconv.ParseBool(sVal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vField.SetBool(bVal)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
fVal, err := strconv.ParseFloat(sVal, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vField.SetFloat(fVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
Ctx context.Context
|
||||
Request *http.Request
|
||||
Writer http.ResponseWriter
|
||||
PathMap map[string]string
|
||||
Bools map[string]bool
|
||||
Strings map[string]string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func NewContext(writer http.ResponseWriter, request *http.Request) *Context {
|
||||
rctx := &Context{
|
||||
Writer: writer,
|
||||
Request: request,
|
||||
Ctx: request.Context(),
|
||||
PathMap: make(map[string]string),
|
||||
Bools: make(map[string]bool),
|
||||
Strings: make(map[string]string),
|
||||
}
|
||||
return rctx
|
||||
}
|
||||
|
||||
// Aux maps
|
||||
func (rctx *Context) SetBool(key string, value bool) {
|
||||
rctx.Bools[key] = value
|
||||
}
|
||||
|
||||
func (rctx *Context) GetBool(key string) (bool, bool) {
|
||||
exists, value := rctx.Bools[key]
|
||||
return exists, value
|
||||
}
|
||||
|
||||
func (rctx *Context) SetString(key string, value string) {
|
||||
rctx.Strings[key] = value
|
||||
}
|
||||
|
||||
func (rctx *Context) GetString(key string) (string, bool) {
|
||||
value, exists := rctx.Strings[key]
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// Request
|
||||
func (rctx *Context) GetSubpath(key string) (string, bool) {
|
||||
value, exists := rctx.PathMap[key]
|
||||
return value, exists
|
||||
}
|
||||
|
||||
func (rctx *Context) URL() *url.URL {
|
||||
return rctx.Request.URL
|
||||
}
|
||||
|
||||
func (rctx *Context) GetQuery(key string) string {
|
||||
return rctx.Request.URL.Query().Get(key)
|
||||
}
|
||||
|
||||
func (rctx *Context) GetHeader(key string) string {
|
||||
return rctx.Request.Header.Get(key)
|
||||
}
|
||||
|
||||
func (rctx *Context) GetHeaders() http.Header {
|
||||
return rctx.Request.Header
|
||||
}
|
||||
|
||||
func (rctx *Context) GetContext() context.Context {
|
||||
return rctx.Request.Context()
|
||||
}
|
||||
|
||||
// Binding
|
||||
const emptyJSON = "{}"
|
||||
|
||||
func (rctx *Context) BindJSON(obj any) error {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
_, err := io.Copy(buffer, rctx.Request.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqBody := buffer.Bytes()
|
||||
if len(reqBody) == 0 {
|
||||
reqBody = []byte(emptyJSON)
|
||||
}
|
||||
err = json.Unmarshal(reqBody, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (rctx *Context) BindQuery(obj any) error {
|
||||
qMap := make(map[string]string)
|
||||
for key, val := range rctx.Request.URL.Query() {
|
||||
if len(val) == 1 {
|
||||
qMap[key] = val[0]
|
||||
}
|
||||
}
|
||||
return bindObj(obj, qMap, "param")
|
||||
}
|
||||
|
||||
// Response
|
||||
func (rctx *Context) SetHeader(key, value string) {
|
||||
rctx.Writer.Header().Set(key, value)
|
||||
}
|
||||
|
||||
func (rctx *Context) SetStatus(httpStatus int) {
|
||||
rctx.StatusCode = httpStatus
|
||||
rctx.Writer.WriteHeader(httpStatus)
|
||||
}
|
||||
|
||||
func (rctx *Context) SendJSON(statusCode int, payload any) {
|
||||
rctx.StatusCode = statusCode
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
json.NewEncoder(buffer).Encode(payload)
|
||||
rctx.Writer.Header().Set("Content-Type", "application/json")
|
||||
size := strconv.FormatInt(int64(len(buffer.Bytes())), 10)
|
||||
rctx.Writer.Header().Set("Content-Length", size)
|
||||
rctx.Writer.WriteHeader(statusCode)
|
||||
rctx.Writer.Write(buffer.Bytes())
|
||||
}
|
||||
|
||||
func (rctx *Context) SendText(statusCode int, payload string) {
|
||||
rctx.StatusCode = statusCode
|
||||
size := strconv.FormatInt(int64(len(payload)), 10)
|
||||
rctx.Writer.Header().Set("Content-Type", "text/plain")
|
||||
rctx.Writer.Header().Set("Content-Length", size)
|
||||
rctx.Writer.WriteHeader(statusCode)
|
||||
rctx.Writer.Write([]byte(payload))
|
||||
}
|
||||
|
||||
func (rctx *Context) SendBytes(statusCode int, ctype string, payload []byte) {
|
||||
rctx.StatusCode = statusCode
|
||||
size := strconv.FormatInt(int64(len(payload)), 10)
|
||||
rctx.Writer.Header().Set("Content-Type", ctype)
|
||||
rctx.Writer.Header().Set("Content-Length", size)
|
||||
rctx.Writer.WriteHeader(statusCode)
|
||||
rctx.Writer.Write(payload)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package router
|
||||
|
||||
func NewCorsMiddleware() MiddlewareFunc {
|
||||
mw := func(next Handler) Handler {
|
||||
return newCorsHandler(next)
|
||||
}
|
||||
return mw
|
||||
}
|
||||
|
||||
type corsHandler struct {
|
||||
next Handler
|
||||
}
|
||||
|
||||
func newCorsHandler(next Handler) *corsHandler {
|
||||
return &corsHandler{
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
func (hand corsHandler) ServeHTTP(ctx *Context) {
|
||||
origin := ctx.Request.Header.Get("Origin")
|
||||
if origin != "" {
|
||||
ctx.SetHeader("Access-Control-Allow-Origin", origin)
|
||||
ctx.SetHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE, PATCH")
|
||||
ctx.SetHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||||
ctx.SetHeader("Access-Control-Max-Age", "86400")
|
||||
ctx.SetHeader("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
if ctx.Request.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
hand.next.ServeHTTP(ctx)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package router
|
||||
|
||||
func NewLoggingMiddleware(print func(string, ...any)) MiddlewareFunc {
|
||||
mw := func(next Handler) Handler {
|
||||
return newLoggingHandler(next, print)
|
||||
}
|
||||
return mw
|
||||
}
|
||||
|
||||
type loggingHandler struct {
|
||||
next Handler
|
||||
printFunc func(string, ...any)
|
||||
}
|
||||
|
||||
func newLoggingHandler(next Handler, print func(string, ...any)) *loggingHandler {
|
||||
return &loggingHandler{
|
||||
next: next,
|
||||
printFunc: print,
|
||||
}
|
||||
}
|
||||
|
||||
func (logging loggingHandler) ServeHTTP(rctx *Context) {
|
||||
logging.next.ServeHTTP(rctx)
|
||||
cl := rctx.Writer.Header().Get("Content-Length")
|
||||
logging.printFunc("%s %s %s %s %s %d", rctx.Request.RemoteAddr,
|
||||
rctx.Request.Method, rctx.Request.URL.String(),
|
||||
rctx.Request.Proto, cl, rctx.StatusCode)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
compContextPlain int = iota
|
||||
compContextRegex
|
||||
|
||||
startRegex byte = '{'
|
||||
stopRegex byte = '}'
|
||||
)
|
||||
|
||||
func pathCompiler(path string) (string, error) {
|
||||
var err error
|
||||
res := make([]byte, 0)
|
||||
var pos int = compContextPlain
|
||||
var depth int = 0
|
||||
pattern := make([]byte, 0)
|
||||
for _, b := range []byte(path) {
|
||||
switch pos {
|
||||
case compContextPlain:
|
||||
switch b {
|
||||
case stopRegex:
|
||||
depth -= 1
|
||||
res = append(res, b)
|
||||
case startRegex:
|
||||
depth += 1
|
||||
pos = compContextRegex // pattern started
|
||||
pattern = make([]byte, 0)
|
||||
default:
|
||||
res = append(res, b)
|
||||
}
|
||||
case compContextRegex:
|
||||
switch b {
|
||||
case startRegex:
|
||||
depth += 1
|
||||
case stopRegex:
|
||||
depth -= 1
|
||||
if depth == 0 {
|
||||
pattern = convertRegexp(pattern)
|
||||
res = append(res, pattern...)
|
||||
pos = compContextPlain // pattern ended
|
||||
}
|
||||
default:
|
||||
pattern = append(pattern, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
if depth != 0 {
|
||||
err = fmt.Errorf("Unbalanced brackets into pattern")
|
||||
}
|
||||
return string(res), err
|
||||
}
|
||||
|
||||
const (
|
||||
defaultRegexp = `[a-zA-Z0-9_\.*][/\-\.,a-zA-Z0-9_%=*\[\]:~\$]+`
|
||||
)
|
||||
|
||||
func convertRegexp(src []byte) []byte {
|
||||
var res string
|
||||
const patternSeps = ":"
|
||||
parts := strings.SplitN(string(src), patternSeps, 2)
|
||||
if len(parts) == 1 {
|
||||
parts = append(parts, defaultRegexp)
|
||||
}
|
||||
res = fmt.Sprintf("(?<%s>%s)", parts[0], parts[1])
|
||||
return []byte(res)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func tDebugf(msg string, args ...any) {
|
||||
fmt.Printf("debug: ")
|
||||
fmt.Printf(msg, args...)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func TestPatchCompilerA(t *testing.T) {
|
||||
var err error
|
||||
srcPath := `/v1/file/{collection:[a-zA-Z]+}/{name}`
|
||||
reSource, err := pathCompiler(srcPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
tDebugf("re: %s\n", reSource)
|
||||
|
||||
re, err := regexp.Compile(reSource)
|
||||
require.NoError(t, err)
|
||||
reqPath := `/v1/file/foo/bare`
|
||||
match := re.MatchString(reqPath)
|
||||
require.True(t, match)
|
||||
|
||||
submatch := re.FindStringSubmatch(reqPath)
|
||||
subnames := re.SubexpNames()
|
||||
|
||||
submap := make(map[string]string)
|
||||
for i, val := range subnames {
|
||||
tDebugf("subname: %d = %s", i, val)
|
||||
}
|
||||
for i, val := range submatch {
|
||||
key := subnames[i]
|
||||
if key != "" {
|
||||
submap[key] = val
|
||||
}
|
||||
tDebugf("sub: %d = %s", i, val)
|
||||
}
|
||||
tDebugf("submap: %v", submap)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewRecoveryMiddleware(print func(string, ...any)) MiddlewareFunc {
|
||||
mw := func(next Handler) Handler {
|
||||
return newRecoveryHandler(next, print)
|
||||
}
|
||||
return mw
|
||||
}
|
||||
|
||||
type recoveryHandler struct {
|
||||
next Handler
|
||||
print func(string, ...any)
|
||||
}
|
||||
|
||||
func newRecoveryHandler(next Handler, print func(string, ...any)) *recoveryHandler {
|
||||
return &recoveryHandler{
|
||||
next: next,
|
||||
print: print,
|
||||
}
|
||||
}
|
||||
|
||||
func (hand recoveryHandler) ServeHTTP(rctx *Context) {
|
||||
exitFunc := func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
rctx.Writer.WriteHeader(http.StatusInternalServerError)
|
||||
stack := string(debug.Stack())
|
||||
timestamp := time.Now().Format(time.RFC3339)
|
||||
hand.print("%s %v ; %s\n", timestamp, err, stack)
|
||||
}
|
||||
}
|
||||
defer exitFunc()
|
||||
hand.next.ServeHTTP(rctx)
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type MiddlewareFunc func(next Handler) Handler
|
||||
|
||||
type Handler interface {
|
||||
ServeHTTP(rctx *Context)
|
||||
}
|
||||
|
||||
type HandlerFunc func(rctx *Context)
|
||||
|
||||
func (handlerFunc HandlerFunc) ServeHTTP(rctx *Context) {
|
||||
handlerFunc(rctx)
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
headHandler Handler
|
||||
routeHandler *Selector
|
||||
}
|
||||
|
||||
func NewRouter() *Router {
|
||||
selector := NewSelector()
|
||||
return &Router{
|
||||
routeHandler: selector,
|
||||
headHandler: selector,
|
||||
}
|
||||
}
|
||||
|
||||
func (rout *Router) Use(mwFunc MiddlewareFunc) {
|
||||
rout.headHandler = mwFunc(rout.headHandler)
|
||||
}
|
||||
|
||||
func (rout *Router) Selector() *Selector {
|
||||
return rout.routeHandler
|
||||
}
|
||||
|
||||
func (rout *Router) AddRoute(method, path string, handlerFunc HandlerFunc) {
|
||||
rout.routeHandler.AddRoute(method, path, handlerFunc)
|
||||
}
|
||||
|
||||
func (rout *Router) Head(path string, handlerFunc HandlerFunc) {
|
||||
rout.routeHandler.AddRoute("HEAD", path, handlerFunc)
|
||||
}
|
||||
|
||||
func (rout *Router) Get(path string, handlerFunc HandlerFunc) {
|
||||
rout.routeHandler.AddRoute("GET", path, handlerFunc)
|
||||
}
|
||||
|
||||
func (rout *Router) Post(path string, handlerFunc HandlerFunc) {
|
||||
rout.routeHandler.AddRoute("POST", path, handlerFunc)
|
||||
}
|
||||
|
||||
func (rout *Router) Put(path string, handlerFunc HandlerFunc) {
|
||||
rout.routeHandler.AddRoute("PUT", path, handlerFunc)
|
||||
}
|
||||
|
||||
func (rout *Router) Patch(path string, handlerFunc HandlerFunc) {
|
||||
rout.routeHandler.AddRoute("PATCH", path, handlerFunc)
|
||||
}
|
||||
|
||||
func (rout *Router) Delete(path string, handlerFunc HandlerFunc) {
|
||||
rout.routeHandler.AddRoute("DELETE", path, handlerFunc)
|
||||
}
|
||||
|
||||
func (rout *Router) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
|
||||
rctx := NewContext(writer, req)
|
||||
rout.headHandler.ServeHTTP(rctx)
|
||||
}
|
||||
|
||||
func (rout *Router) NotFound(handlerFunc HandlerFunc) {
|
||||
rout.routeHandler.notFound = handlerFunc
|
||||
}
|
||||
|
||||
type Selector struct {
|
||||
Routes []*Route
|
||||
notFound Handler
|
||||
}
|
||||
|
||||
func NewSelector() *Selector {
|
||||
notFound := HandlerFunc(func(ctx *Context) {
|
||||
http.NotFound(ctx.Writer, ctx.Request)
|
||||
})
|
||||
return &Selector{
|
||||
Routes: make([]*Route, 0),
|
||||
notFound: notFound,
|
||||
}
|
||||
}
|
||||
|
||||
const globalRoutePattern = `^%s$`
|
||||
|
||||
func (hand *Selector) AddRoute(method, path string, handlerFunc HandlerFunc) error {
|
||||
var err error
|
||||
rawPath := path
|
||||
path, err = pathCompiler(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path = fmt.Sprintf(globalRoutePattern, path)
|
||||
re, err := regexp.Compile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
route := &Route{
|
||||
Method: method,
|
||||
Path: path,
|
||||
RawPath: rawPath,
|
||||
Handler: handlerFunc,
|
||||
Regexp: re,
|
||||
}
|
||||
hand.Routes = append(hand.Routes, route)
|
||||
return err
|
||||
}
|
||||
|
||||
func (hand *Selector) ServeHTTP(rctx *Context) {
|
||||
realHandler := hand.notFound
|
||||
for _, route := range hand.Routes {
|
||||
match := route.Match(rctx.Request)
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
subvals := route.Regexp.FindStringSubmatch(rctx.Request.URL.Path)
|
||||
subkeys := route.Regexp.SubexpNames()
|
||||
for i, val := range subvals {
|
||||
key := subkeys[i]
|
||||
if key != "" {
|
||||
rctx.PathMap[key] = val
|
||||
}
|
||||
}
|
||||
realHandler = route.Handler
|
||||
break
|
||||
}
|
||||
realHandler.ServeHTTP(rctx)
|
||||
}
|
||||
|
||||
type Route struct {
|
||||
Method string
|
||||
Regexp *regexp.Regexp
|
||||
Path string
|
||||
RawPath string
|
||||
Handler HandlerFunc
|
||||
}
|
||||
|
||||
func (route Route) Match(req *http.Request) bool {
|
||||
if req.Method != route.Method {
|
||||
return false
|
||||
}
|
||||
match := route.Regexp.MatchString(req.URL.Path)
|
||||
if match {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRouterStatus(t *testing.T) {
|
||||
|
||||
reqPath := "/hello"
|
||||
handler := NewRouter()
|
||||
helloHandler := func(rctx *Context) {
|
||||
rctx.SetStatus(http.StatusOK)
|
||||
}
|
||||
handler.Get(reqPath, helloHandler)
|
||||
|
||||
request, err := http.NewRequest("GET", reqPath, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
fmt.Printf("Response code: %d\n", recorder.Code)
|
||||
}
|
||||
|
||||
func TestRouterSendText(t *testing.T) {
|
||||
testText := "hello, world"
|
||||
reqPath := "/hello"
|
||||
rout := NewRouter()
|
||||
helloHandler := func(rctx *Context) {
|
||||
rctx.SendText(http.StatusOK, testText)
|
||||
}
|
||||
rout.Get(reqPath, helloHandler)
|
||||
|
||||
request, err := http.NewRequest("GET", reqPath, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
rout.ServeHTTP(recorder, request)
|
||||
fmt.Printf("Response code: %d\n", recorder.Code)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
bodyReader := recorder.Body
|
||||
bodyBytes, err := io.ReadAll(bodyReader)
|
||||
|
||||
fmt.Printf("Response body: %s\n", string(bodyBytes))
|
||||
require.Equal(t, string(bodyBytes), testText)
|
||||
}
|
||||
|
||||
func TestRouterSendJSON(t *testing.T) {
|
||||
|
||||
type testStruct struct {
|
||||
Message string `json:"message"`
|
||||
Code int64 `json:"code"`
|
||||
}
|
||||
testVar := testStruct{
|
||||
Message: "hello, world",
|
||||
Code: 123,
|
||||
}
|
||||
testData, err := json.Marshal(testVar)
|
||||
require.NoError(t, err)
|
||||
|
||||
reqPath := "/hello"
|
||||
handler := NewRouter()
|
||||
|
||||
helloHandler := func(rctx *Context) {
|
||||
rctx.SendJSON(http.StatusOK, &testVar)
|
||||
}
|
||||
handler.Get(reqPath, helloHandler)
|
||||
|
||||
request, err := http.NewRequest("GET", reqPath, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
fmt.Printf("Response code: %d\n", recorder.Code)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
bodyReader := recorder.Body
|
||||
bodyBytes, err := io.ReadAll(bodyReader)
|
||||
require.NoError(t, err)
|
||||
bodyBytes = bytes.Trim(bodyBytes, "\n\r")
|
||||
|
||||
fmt.Printf("Response body: %s\n", string(bodyBytes))
|
||||
require.Equal(t, string(testData), string(bodyBytes))
|
||||
}
|
||||
|
||||
func TestRouterBindJSON(t *testing.T) {
|
||||
|
||||
type testStruct struct {
|
||||
Message string `json:"message"`
|
||||
Code int64 `json:code"`
|
||||
}
|
||||
testVar := testStruct{
|
||||
Message: "hello, world",
|
||||
Code: 123,
|
||||
}
|
||||
testData, err := json.Marshal(testVar)
|
||||
require.NoError(t, err)
|
||||
buffer := bytes.NewBuffer(testData)
|
||||
|
||||
reqPath := "/hello"
|
||||
handler := NewRouter()
|
||||
|
||||
helloHandler := func(rctx *Context) {
|
||||
handVar := testStruct{}
|
||||
rctx.BindJSON(&handVar)
|
||||
fmt.Printf("Received message: %s - %d\n", handVar.Message, handVar.Code)
|
||||
require.Equal(t, handVar.Code, int64(123))
|
||||
rctx.SendJSON(http.StatusOK, &handVar)
|
||||
}
|
||||
handler.Post(reqPath, helloHandler)
|
||||
|
||||
request, err := http.NewRequest("POST", reqPath, buffer)
|
||||
require.NoError(t, err)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
|
||||
fmt.Printf("Response code: %d\n", recorder.Code)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
bodyReader := recorder.Body
|
||||
bodyBytes, err := io.ReadAll(bodyReader)
|
||||
require.NoError(t, err)
|
||||
bodyBytes = bytes.Trim(bodyBytes, "\n\r")
|
||||
|
||||
fmt.Printf("Response body: %s\n", string(bodyBytes))
|
||||
require.Equal(t, string(testData), string(bodyBytes))
|
||||
}
|
||||
|
||||
func TestRouterBindParams(t *testing.T) {
|
||||
|
||||
reqPath := "/hello"
|
||||
handler := NewRouter()
|
||||
|
||||
helloHandler := func(rctx *Context) {
|
||||
type Params struct {
|
||||
Name string `param:"name"`
|
||||
Code int64 `param:"code"`
|
||||
}
|
||||
params := &Params{}
|
||||
rctx.BindQuery(params)
|
||||
fmt.Printf("Received name: %s\n", params.Name)
|
||||
fmt.Printf("Received code: %d\n", params.Code)
|
||||
rctx.SendText(http.StatusOK, "hello")
|
||||
}
|
||||
handler.Get(reqPath, helloHandler)
|
||||
|
||||
reqPath = reqPath + `?name=world&code=123`
|
||||
request, err := http.NewRequest("GET", reqPath, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
|
||||
fmt.Printf("Response code: %d\n", recorder.Code)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
bodyReader := recorder.Body
|
||||
bodyBytes, err := io.ReadAll(bodyReader)
|
||||
require.NoError(t, err)
|
||||
bodyBytes = bytes.Trim(bodyBytes, "\n\r")
|
||||
|
||||
fmt.Printf("Response body: %s\n", string(bodyBytes))
|
||||
}
|
||||
|
||||
func BenchmarkLoggerL(b *testing.B) {
|
||||
reqPath := "/hello"
|
||||
helloHandler := func(rctx *Context) {
|
||||
rctx.SetStatus(http.StatusOK)
|
||||
}
|
||||
handler := NewRouter()
|
||||
handler.Get(reqPath, helloHandler)
|
||||
|
||||
request, err := http.NewRequest("GET", reqPath, nil)
|
||||
require.NoError(b, err)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
require.Equal(b, http.StatusOK, recorder.Code)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
)
|
||||
|
||||
const protocol = "tcp"
|
||||
|
||||
func CreateTLSListener(addrinfo string, x509cert, x509key []byte) (net.Listener, error) {
|
||||
var listen net.Listener
|
||||
var err error
|
||||
tlsCert, err := tls.X509KeyPair(x509cert, x509key)
|
||||
if err != nil {
|
||||
return listen, err
|
||||
}
|
||||
tlsConfig := tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
ClientAuth: tls.NoClientCert,
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
listen, err = tls.Listen(protocol, addrinfo, &tlsConfig)
|
||||
if err != nil {
|
||||
return listen, err
|
||||
}
|
||||
return listen, err
|
||||
}
|
||||
|
||||
func CreateListener(addrinfo string) (net.Listener, error) {
|
||||
var listen net.Listener
|
||||
var err error
|
||||
listen, err = net.Listen(protocol, addrinfo)
|
||||
if err != nil {
|
||||
return listen, err
|
||||
}
|
||||
return listen, err
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"mbase/app/config"
|
||||
"mbase/app/handler"
|
||||
"mbase/app/logger"
|
||||
"mbase/app/maindb"
|
||||
"mbase/app/service"
|
||||
"mbase/pkg/auxtool"
|
||||
"mbase/pkg/descr"
|
||||
|
||||
"mbase/app/accoper"
|
||||
"mbase/app/servoper"
|
||||
|
||||
yaml "go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
conf *config.Config
|
||||
acop *accoper.Operator
|
||||
seop *servoper.Operator
|
||||
svc *service.Service
|
||||
mdb *maindb.Database
|
||||
hand *handler.Handler
|
||||
logg *logger.Logger
|
||||
stat descr.Server
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
logf *os.File
|
||||
//x509cert []byte
|
||||
//x509key []byte
|
||||
listen net.Listener
|
||||
}
|
||||
|
||||
func NewServer() (*Server, error) {
|
||||
var err error
|
||||
srv := &Server{}
|
||||
srv.logg = logger.NewLoggerWithSubject("server")
|
||||
srv.ctx, srv.cancel = context.WithCancel(context.Background())
|
||||
return srv, err
|
||||
}
|
||||
|
||||
func (srv *Server) Handler() *handler.Handler {
|
||||
return srv.hand
|
||||
}
|
||||
|
||||
func (srv *Server) Service() *service.Service {
|
||||
return srv.svc
|
||||
}
|
||||
|
||||
func (srv *Server) SetLogdir(dir string) {
|
||||
srv.conf.Logpath = dir
|
||||
}
|
||||
|
||||
func (srv *Server) SetRundir(dir string) {
|
||||
srv.conf.Runpath = dir
|
||||
}
|
||||
|
||||
func (srv *Server) SetDatadir(dir string) {
|
||||
srv.conf.Database.Basepath = dir
|
||||
srv.conf.Storage.Basepath = dir
|
||||
srv.conf.Datadir = dir
|
||||
}
|
||||
|
||||
func (srv *Server) SetPort(port uint32) {
|
||||
srv.conf.Service.Port = port
|
||||
}
|
||||
|
||||
func (srv *Server) SetAsDaemon(asDaemon bool) {
|
||||
srv.conf.AsDaemon = asDaemon
|
||||
}
|
||||
|
||||
func (srv *Server) Configure() error {
|
||||
var err error
|
||||
//srv.logg.Infof("Configuration server")
|
||||
srv.conf = config.NewConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = srv.conf.ReadConfigfile()
|
||||
if err != nil {
|
||||
srv.logg.Warningf("Error loading config file: %v", err)
|
||||
err = nil
|
||||
}
|
||||
err = srv.conf.ReadX509Cert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (srv *Server) writeStat() error {
|
||||
// Write status file
|
||||
var err error
|
||||
statefilePath := filepath.Join(srv.conf.Datadir, "server.yaml")
|
||||
stateData, err := yaml.Marshal(srv.stat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(statefilePath, stateData, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (srv *Server) readStat() error {
|
||||
var err error
|
||||
// Read state file
|
||||
statefilePath := filepath.Join(srv.conf.Datadir, "server.yaml")
|
||||
if auxtool.FileExists(statefilePath) {
|
||||
stateData, err := ioutil.ReadFile(statefilePath)
|
||||
if err == nil {
|
||||
stat := descr.Server{}
|
||||
err = yaml.Unmarshal(stateData, &stat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.stat = stat
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (srv *Server) Build() error {
|
||||
var err error
|
||||
//srv.logg.Infof("Server building")
|
||||
|
||||
currUser, err := user.Current()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error getting current user: %v\n", err)
|
||||
return err
|
||||
}
|
||||
cuid64, err := strconv.ParseInt(currUser.Uid, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cgid64, err := strconv.ParseInt(currUser.Gid, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
euid := int(cuid64)
|
||||
egid := int(cgid64)
|
||||
if cuid64 == 0 {
|
||||
usr, err := user.Lookup(srv.conf.RunUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uid64, err := strconv.ParseInt(usr.Uid, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gid64, err := strconv.ParseInt(usr.Gid, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
euid = int(uid64)
|
||||
egid = int(gid64)
|
||||
}
|
||||
// Creating datadir
|
||||
datadir := srv.conf.Datadir
|
||||
if !auxtool.DirExists(datadir) { // TODO: check access to dir
|
||||
//srv.logg.Infof("Creating data directory %s ", datadir)
|
||||
err = os.MkdirAll(datadir, 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = os.Chown(datadir, euid, egid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if srv.conf.AsDaemon {
|
||||
logdir := filepath.Dir(srv.conf.Logpath)
|
||||
//srv.logg.Infof("Creating log directory %s", logdir)
|
||||
err = os.MkdirAll(logdir, 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Chown(logdir, euid, egid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rundir := filepath.Dir(srv.conf.Runpath)
|
||||
//srv.logg.Infof("Creating run directory %s", rundir)
|
||||
err = os.MkdirAll(rundir, 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Chown(rundir, euid, egid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Redirect stderr and stout
|
||||
logFile, err := os.OpenFile(srv.conf.Logpath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = syscall.Dup2(int(logFile.Fd()), int(os.Stdout.Fd()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = syscall.Dup2(int(logFile.Fd()), int(os.Stderr.Fd()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.logf = logFile
|
||||
}
|
||||
|
||||
confDump := srv.conf.String()
|
||||
srv.logg.Infof("Current server configuration is:\n%s\n", confDump)
|
||||
|
||||
// Read state file
|
||||
srv.logg.Infof("Reading server status")
|
||||
err = srv.readStat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Creating database dir
|
||||
dbdir := srv.conf.Database.Basepath
|
||||
srv.logg.Infof("Creating database directory %s ", dbdir)
|
||||
err = os.MkdirAll(dbdir, 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Creating storage dir
|
||||
srv.logg.Infof("Creating storage directory")
|
||||
datadir = srv.conf.Database.Basepath
|
||||
err = os.MkdirAll(datadir, 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Chown(datadir, euid, egid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert, key := []byte(srv.conf.X509Cert), []byte(srv.conf.X509Key)
|
||||
addrinfo := fmt.Sprintf("%s:%d", srv.conf.Service.Address, srv.conf.Service.Port)
|
||||
listener, err := CreateTLSListener(addrinfo, cert, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.listen = listener
|
||||
|
||||
if cuid64 == 0 {
|
||||
// Change effective user and group
|
||||
err = syscall.Setgid(egid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = syscall.Setuid(euid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
//return fmt.Errorf("Debug break")
|
||||
|
||||
uidstr := strconv.FormatInt(int64(syscall.Geteuid()), 10)
|
||||
usr, err := user.LookupId(uidstr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.logg.Warningf("Now run as user: %s", usr.Username)
|
||||
// Creating database
|
||||
mdb := maindb.NewDatabase(dbdir)
|
||||
srv.logg.Infof("Opening main database")
|
||||
err = mdb.OpenDatabase()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.mdb = mdb
|
||||
|
||||
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
// Created db scheme
|
||||
if !srv.stat.SchemeCreated {
|
||||
srv.logg.Infof("Initialize main database")
|
||||
err = mdb.InitDatabase()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.stat.SchemeCreated = true
|
||||
srv.stat.SchemeCreatedAt = auxtool.TimeNow()
|
||||
srv.logg.Infof("Writing server status")
|
||||
err = srv.writeStat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Created anonymous user
|
||||
if !srv.stat.AnonymousCreated {
|
||||
srv.logg.Infof("Creating anonymous user")
|
||||
err = mdb.WriteAnonymous(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.stat.AnonymousCreated = true
|
||||
srv.stat.AnonymousCreatedAt = auxtool.TimeNow()
|
||||
srv.logg.Infof("Writing server status")
|
||||
err = srv.writeStat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !srv.stat.InituserCreated {
|
||||
srv.logg.Infof("Creating init user")
|
||||
err = srv.mdb.WriteInituser(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.stat.InituserCreatedAt = auxtool.TimeNow()
|
||||
srv.stat.InituserCreated = true
|
||||
|
||||
srv.logg.Infof("Writing server status")
|
||||
err = srv.writeStat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Creating account operator
|
||||
srv.logg.Infof("Creating account operator")
|
||||
accoperParams := &accoper.OperatorParams{
|
||||
MainDB: srv.mdb,
|
||||
}
|
||||
srv.acop, err = accoper.NewOperator(accoperParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Creating service operator
|
||||
srv.logg.Infof("Creating operator")
|
||||
servoperParams := &servoper.OperatorParams{
|
||||
MainDB: srv.mdb,
|
||||
}
|
||||
srv.seop, err = servoper.NewOperator(servoperParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Creating handler
|
||||
srv.logg.Infof("Creating handler")
|
||||
handlerParams := &handler.HandlerParams{
|
||||
MainDB: srv.mdb,
|
||||
AccOper: srv.acop,
|
||||
ServOper: srv.seop,
|
||||
}
|
||||
srv.hand, err = handler.NewHandler(handlerParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Creating service
|
||||
serviceParams := &service.ServiceParams{
|
||||
Handler: srv.hand,
|
||||
Listener: srv.listen,
|
||||
}
|
||||
srv.logg.Infof("Creating service")
|
||||
srv.svc, err = service.NewService(serviceParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Building service
|
||||
err = srv.svc.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (srv *Server) Run() error {
|
||||
var err error
|
||||
|
||||
if srv.conf.AsDaemon {
|
||||
// Redirect stdin
|
||||
nullFile, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stdin.Fd()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Write process ID
|
||||
pidFile, err := os.OpenFile(srv.conf.Runpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pidFile.Close()
|
||||
currPid := os.Getpid()
|
||||
_, err = pidFile.WriteString(strconv.Itoa(currPid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Start log rotator
|
||||
srv.Rotator()
|
||||
}
|
||||
|
||||
currUser, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.logg.Infof("Server started with user: %s", currUser.Username)
|
||||
uidstr := strconv.FormatInt(int64(syscall.Geteuid()), 10)
|
||||
usr, err := user.LookupId(uidstr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.logg.Infof("Server run with user: %s", usr.Username)
|
||||
|
||||
svcDone := make(chan error, 1)
|
||||
// Service run
|
||||
srv.logg.Infof("Start service")
|
||||
startService := func(svc *service.Service, done chan error) {
|
||||
err = svc.Run()
|
||||
if err != nil {
|
||||
srv.logg.Errorf("Service error: %v", err)
|
||||
done <- err
|
||||
}
|
||||
}
|
||||
go startService(srv.svc, svcDone)
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
var signal os.Signal
|
||||
|
||||
select {
|
||||
case signal = <-sigs:
|
||||
srv.logg.Infof("Services stopped by signal: %v", signal)
|
||||
srv.cancel()
|
||||
srv.svc.Stop()
|
||||
srv.wg.Wait()
|
||||
case err = <-svcDone:
|
||||
srv.logg.Infof("Service stopped by service error: %v", err)
|
||||
srv.cancel()
|
||||
srv.svc.Stop()
|
||||
srv.wg.Wait()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (srv *Server) PseudoFork() error {
|
||||
const successExit int = 0
|
||||
var keyEnv string = "IMX0LTSELMRF8K"
|
||||
var err error
|
||||
|
||||
_, isChild := os.LookupEnv(keyEnv)
|
||||
switch {
|
||||
case !isChild:
|
||||
os.Setenv(keyEnv, "TRUE")
|
||||
|
||||
procAttr := syscall.ProcAttr{}
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var sysFiles = make([]uintptr, 3)
|
||||
sysFiles[0] = uintptr(syscall.Stdin)
|
||||
sysFiles[1] = uintptr(syscall.Stdout)
|
||||
sysFiles[2] = uintptr(syscall.Stderr)
|
||||
|
||||
procAttr.Files = sysFiles
|
||||
procAttr.Env = os.Environ()
|
||||
procAttr.Dir = cwd
|
||||
|
||||
_, err = syscall.ForkExec(os.Args[0], os.Args, &procAttr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Exit(successExit)
|
||||
case isChild:
|
||||
_, err = syscall.Setsid()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
os.Unsetenv(keyEnv)
|
||||
return err
|
||||
}
|
||||
|
||||
func (srv *Server) Daemonize() error {
|
||||
var err error
|
||||
if srv.conf.AsDaemon {
|
||||
// Restart process process
|
||||
err = srv.PseudoFork()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (srv *Server) Rotator() {
|
||||
srv.wg.Add(1)
|
||||
var counter uint64
|
||||
logFunc := func() {
|
||||
for {
|
||||
counter += 1
|
||||
select {
|
||||
case <-srv.ctx.Done():
|
||||
srv.wg.Done()
|
||||
srv.logg.Infof("Log file rotator done")
|
||||
return
|
||||
default:
|
||||
}
|
||||
if (counter % 60) == 1 {
|
||||
stat, err := srv.logf.Stat()
|
||||
if err == nil && stat.Size() > srv.conf.LogLimit {
|
||||
srv.logg.Infof("Rotate log file")
|
||||
countFiles := 3
|
||||
for i := 1; i < countFiles; i++ {
|
||||
nextName := fmt.Sprintf("%s.%d", srv.conf.Logpath, i+1)
|
||||
prevName := fmt.Sprintf("%s.%d", srv.conf.Logpath, i)
|
||||
os.Rename(prevName, nextName)
|
||||
}
|
||||
os.Rename(srv.conf.Logpath, srv.conf.Logpath+".1")
|
||||
logFile, err := os.OpenFile(srv.conf.Logpath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0640)
|
||||
if err == nil {
|
||||
syscall.Dup2(int(logFile.Fd()), int(os.Stdout.Fd()))
|
||||
syscall.Dup2(int(logFile.Fd()), int(os.Stderr.Fd()))
|
||||
srv.logf.Close()
|
||||
srv.logf = logFile
|
||||
}
|
||||
}
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
go logFunc()
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"mbase/app/handler"
|
||||
"mbase/app/logger"
|
||||
"mbase/app/router"
|
||||
)
|
||||
|
||||
type ServiceParams struct {
|
||||
Handler *handler.Handler
|
||||
Listener net.Listener
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
hand *handler.Handler
|
||||
rout *router.Router
|
||||
logg *logger.Logger
|
||||
listen net.Listener
|
||||
hsrv *http.Server
|
||||
}
|
||||
|
||||
func NewService(params *ServiceParams) (*Service, error) {
|
||||
var err error
|
||||
svc := &Service{
|
||||
hand: params.Handler,
|
||||
listen: params.Listener,
|
||||
}
|
||||
svc.logg = logger.NewLoggerWithSubject("service")
|
||||
return svc, err
|
||||
}
|
||||
|
||||
func (svc *Service) Build() error {
|
||||
var err error
|
||||
svc.logg.Infof("Service build ")
|
||||
|
||||
svc.rout = router.NewRouter()
|
||||
|
||||
svc.rout.Use(router.NewRecoveryMiddleware(svc.logg.Errorf))
|
||||
svc.rout.Use(router.NewLoggingMiddleware(svc.logg.Infof))
|
||||
svc.rout.Use(router.NewCorsMiddleware())
|
||||
svc.rout.Use(svc.hand.AuthMiddleware)
|
||||
|
||||
svc.rout.Get(`/v3/api/service/hello`, svc.hand.SendHello)
|
||||
|
||||
svc.rout.Post(`/v3/api/account/create`, svc.hand.CreateAccount)
|
||||
svc.rout.Post(`/v3/api/account/get`, svc.hand.GetAccount)
|
||||
svc.rout.Post(`/v3/api/account/update`, svc.hand.UpdateAccount)
|
||||
svc.rout.Post(`/v3/api/account/delete`, svc.hand.DeleteAccount)
|
||||
svc.rout.Post(`/v3/api/accounts/list`, svc.hand.ListAccounts)
|
||||
|
||||
svc.rout.Post(`/v3/api/grant/create`, svc.hand.CreateGrant)
|
||||
svc.rout.Post(`/v3/api/grant/get`, svc.hand.GetGrant)
|
||||
svc.rout.Post(`/v3/api/grant/update`, svc.hand.UpdateGrant)
|
||||
svc.rout.Post(`/v3/api/grant/delete`, svc.hand.DeleteGrant)
|
||||
svc.rout.Post(`/v3/api/grants/list`, svc.hand.ListGrants)
|
||||
|
||||
svc.rout.NotFound(svc.hand.NotFound)
|
||||
|
||||
selector := svc.rout.Selector()
|
||||
for _, item := range selector.Routes {
|
||||
svc.logg.Infof("%s\t%s", item.Method, item.RawPath)
|
||||
}
|
||||
|
||||
svc.logg.Infof("Service listening at %v", svc.listen.Addr())
|
||||
svc.hsrv = &http.Server{
|
||||
Handler: svc.rout,
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (svc *Service) Run() error {
|
||||
var err error
|
||||
svc.logg.Infof("Service run")
|
||||
err = svc.hsrv.Serve(svc.listen)
|
||||
if err == http.ErrServerClosed {
|
||||
svc.logg.Warningf("Service Closed")
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (svc *Service) Stop() {
|
||||
if svc.hsrv != nil {
|
||||
svc.logg.Infof("Service stop")
|
||||
downWaiting := 10 * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), downWaiting)
|
||||
defer cancel()
|
||||
err := svc.hsrv.Shutdown(ctx)
|
||||
if err != nil {
|
||||
svc.logg.Errorf("Error service shutdown: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package servoper
|
||||
|
||||
import (
|
||||
"mbase/app/logger"
|
||||
"mbase/app/maindb"
|
||||
)
|
||||
|
||||
type OperatorParams struct {
|
||||
MainDB *maindb.Database
|
||||
}
|
||||
|
||||
type Operator struct {
|
||||
mdb *maindb.Database
|
||||
logg *logger.Logger
|
||||
}
|
||||
|
||||
func NewOperator(params *OperatorParams) (*Operator, error) {
|
||||
var err error
|
||||
oper := &Operator{
|
||||
mdb: params.MainDB,
|
||||
}
|
||||
oper.logg = logger.NewLoggerWithSubject("servoper")
|
||||
return oper, err
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
|
||||
*/
|
||||
package servoper
|
||||
|
||||
type SendHelloParams struct{}
|
||||
|
||||
type SendHelloResult struct {
|
||||
Message string `json:"message"`
|
||||
Alive bool `json:"alive"`
|
||||
}
|
||||
|
||||
func (oper *Operator) SendHello(param *SendHelloParams) (*SendHelloResult, error) {
|
||||
var err error
|
||||
res := &SendHelloResult{
|
||||
Alive: true,
|
||||
Message: "hello",
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
Reference in New Issue
Block a user