init import

This commit is contained in:
Олег Бородин
2026-05-24 11:02:51 +02:00
commit 25365aef77
154 changed files with 21501 additions and 0 deletions
+64
View File
@@ -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
}
+97
View File
@@ -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
}
+66
View File
@@ -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
}
+41
View File
@@ -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
}
+78
View File
@@ -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
}
+40
View File
@@ -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
}
+44
View File
@@ -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
}
+56
View File
@@ -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
}
+27
View File
@@ -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
}
+80
View File
@@ -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
}
+52
View File
@@ -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
}
+159
View File
@@ -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
}
+10
View File
@@ -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"
)
+103
View File
@@ -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
}
+177
View File
@@ -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)
}
+177
View File
@@ -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)
}
+45
View File
@@ -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))
}
+15
View File
@@ -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)
}
+36
View File
@@ -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)
}
+15
View File
@@ -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)
}
+66
View File
@@ -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()
}
+62
View File
@@ -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()
}
+84
View File
@@ -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()
}
+33
View File
@@ -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")
}
})
}
+113
View File
@@ -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
}
+149
View File
@@ -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
}
+58
View File
@@ -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)
}
+83
View File
@@ -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
}
+50
View File
@@ -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
}
+43
View File
@@ -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);
`
+57
View File
@@ -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
}
+150
View File
@@ -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)
}
+36
View File
@@ -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)
}
+31
View File
@@ -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)
}
+74
View File
@@ -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)
}
+49
View File
@@ -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)
}
+43
View File
@@ -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)
}
+162
View File
@@ -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
}
+195
View File
@@ -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)
}
}
+37
View File
@@ -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
}
+547
View File
@@ -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()
}
+104
View File
@@ -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)
}
}
}
+27
View File
@@ -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
}
+20
View File
@@ -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
}