This commit is contained in:
2024-04-21 14:42:52 +02:00
parent 4b69674ede
commit 8a25f53c99
10700 changed files with 55767 additions and 14201 deletions

View File

@@ -0,0 +1,44 @@
root = "."
testdata_dir = "testdata"
tmp_dir = ".air"
[build]
args_bin = []
bin = "./.air/main"
cmd = "go build -o ./.air/main ."
delay = 0
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

View File

@@ -0,0 +1,6 @@
README.md
DEVELOPMENT.md
Dockerfile
.gitignore
.gitattributes
.vscode

View File

@@ -0,0 +1,39 @@
PORT=5000
# URLs
PUBLIC_UI_URL="http://localhost:3000"
CONVERSION_URL="http://127.0.0.1:5001"
POSTGRES_URL="postgresql://voltaserve:voltaserve@127.0.0.1:5432/voltaserve"
# Security
SECURITY_JWT_SIGNING_KEY="586cozl1x9m6zmu4fg8iwi6ajazguehcm9qdfgd5ndo2pc3pcn"
SECURITY_CORS_ORIGINS="http://localhost:3000"
SECURITY_API_KEY="7znl9Zd8F!4lRZA43lEQb757mCy"
# S3
S3_URL="127.0.0.1:9000"
S3_ACCESS_KEY="voltaserve"
S3_SECRET_KEY="voltaserve"
S3_REGION="us-east-1"
S3_SECURE=false
# Search
SEARCH_URL="http://127.0.0.1:7700"
# Redis
REDIS_ADDRESS="127.0.0.1:6379"
REDIS_DB=0
# SMTP
SMTP_HOST="localhost"
SMTP_PORT=1025
SMTP_SECURE=false
SMTP_SENDER_ADDRESS="no-reply@localhost"
SMTP_SENDER_NAME="Voltaserve"
# Limits
LIMITS_EXTERNAL_COMMAND_TIMEOUT_SECONDS=900
LIMITS_MULTIPART_BODY_LENGTH_LIMIT_MB=1000
# Defaults
DEFAULT_WORKSPACE_STORAGE_CAPACITY_BYTES=100000000000

View File

@@ -0,0 +1 @@
docs/** linguist-generated

4
Downloads/Voltaserve/api/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/.env.local
/voltaserve
/__debug_bin*
/.air

View File

@@ -0,0 +1,5 @@
{
"recommendations": [
"golang.go"
]
}

View File

@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go"
}
]
}

View File

@@ -0,0 +1,6 @@
{
"[go]": {
"editor.defaultFormatter": "golang.go",
"editor.tabSize": 4
}
}

View File

@@ -0,0 +1,20 @@
FROM registry.suse.com/bci/golang:1.21 AS builder
WORKDIR /build
COPY . .
RUN go mod download
RUN go build -o voltaserve-api
FROM registry.suse.com/bci/bci-micro:15.5 AS runner
WORKDIR /app
COPY --from=builder /build/voltaserve-api ./voltaserve-api
COPY --from=builder /build/.env ./.env
COPY --from=builder /build/templates ./templates
ENTRYPOINT ["./voltaserve-api"]
EXPOSE 5000

View File

@@ -0,0 +1,55 @@
# Voltaserve API
## Getting Started
Install [swag](https://github.com/swaggo/swag) and [golangci-lint](https://golangci-lint.run/usage/install).
Run for development:
```shell
go run .
```
Build binary:
```shell
go build .
```
Build Docker image:
```shell
docker build -t voltaserve/api .
```
Run code linter:
```shell
golangci-lint run
```
## Generate and Publish Documentation
Format swag comments:
```shell
swag fmt
```
Generate `swagger.yml`:
```shell
swag init --output ./docs --outputTypes yaml
```
Preview (will be served at [http://127.0.0.1:5555](http://127.0.0.1:5555)):
```shell
npx @redocly/cli preview-docs --port 5555 ./docs/swagger.yaml
```
Generate the final static HTML documentation:
```shell
npx @redocly/cli build-docs ./docs/swagger.yaml --output ./docs/index.html
```

View File

@@ -0,0 +1,64 @@
package cache
import (
"encoding/json"
"voltaserve/infra"
"voltaserve/model"
"voltaserve/repo"
)
type FileCache struct {
redis *infra.RedisManager
fileRepo repo.FileRepo
keyPrefix string
}
func NewFileCache() *FileCache {
return &FileCache{
fileRepo: repo.NewFileRepo(),
redis: infra.NewRedisManager(),
keyPrefix: "file:",
}
}
func (c *FileCache) Set(file model.File) error {
b, err := json.Marshal(file)
if err != nil {
return err
}
err = c.redis.Set(c.keyPrefix+file.GetID(), string(b))
if err != nil {
return err
}
return nil
}
func (c *FileCache) Get(id string) (model.File, error) {
value, err := c.redis.Get(c.keyPrefix + id)
if err != nil {
return c.Refresh(id)
}
file := repo.NewFile()
if err = json.Unmarshal([]byte(value), &file); err != nil {
return nil, err
}
return file, nil
}
func (c *FileCache) Refresh(id string) (model.File, error) {
res, err := c.fileRepo.Find(id)
if err != nil {
return nil, err
}
if err = c.Set(res); err != nil {
return nil, err
}
return res, nil
}
func (c *FileCache) Delete(id string) error {
if err := c.redis.Delete(c.keyPrefix + id); err != nil {
return nil
}
return nil
}

View File

@@ -0,0 +1,57 @@
package cache
import (
"encoding/json"
"voltaserve/infra"
"voltaserve/model"
"voltaserve/repo"
)
type GroupCache struct {
redis *infra.RedisManager
groupRepo repo.GroupRepo
keyPrefix string
}
func NewGroupCache() *GroupCache {
return &GroupCache{
redis: infra.NewRedisManager(),
groupRepo: repo.NewGroupRepo(),
keyPrefix: "group:",
}
}
func (c *GroupCache) Set(workspace model.Group) error {
b, err := json.Marshal(workspace)
if err != nil {
return err
}
err = c.redis.Set(c.keyPrefix+workspace.GetID(), string(b))
if err != nil {
return err
}
return nil
}
func (c *GroupCache) Get(id string) (model.Group, error) {
value, err := c.redis.Get(c.keyPrefix + id)
if err != nil {
return c.Refresh(id)
}
group := repo.NewGroup()
if err = json.Unmarshal([]byte(value), &group); err != nil {
return nil, err
}
return group, nil
}
func (c *GroupCache) Refresh(id string) (model.Group, error) {
res, err := c.groupRepo.Find(id)
if err != nil {
return nil, err
}
if err = c.Set(res); err != nil {
return nil, err
}
return res, nil
}

View File

@@ -0,0 +1,64 @@
package cache
import (
"encoding/json"
"voltaserve/infra"
"voltaserve/model"
"voltaserve/repo"
)
type OrganizationCache struct {
redis *infra.RedisManager
orgRepo repo.OrganizationRepo
keyPrefix string
}
func NewOrganizationCache() *OrganizationCache {
return &OrganizationCache{
redis: infra.NewRedisManager(),
orgRepo: repo.NewOrganizationRepo(),
keyPrefix: "organization:",
}
}
func (c *OrganizationCache) Set(organization model.Organization) error {
b, err := json.Marshal(organization)
if err != nil {
return err
}
err = c.redis.Set(c.keyPrefix+organization.GetID(), string(b))
if err != nil {
return err
}
return nil
}
func (c *OrganizationCache) Get(id string) (model.Organization, error) {
value, err := c.redis.Get(c.keyPrefix + id)
if err != nil {
return c.Refresh(id)
}
var org = repo.NewOrganization()
if err = json.Unmarshal([]byte(value), &org); err != nil {
return nil, err
}
return org, nil
}
func (c *OrganizationCache) Refresh(id string) (model.Organization, error) {
res, err := c.orgRepo.Find(id)
if err != nil {
return nil, err
}
if err = c.Set(res); err != nil {
return nil, err
}
return res, nil
}
func (c *OrganizationCache) Delete(id string) error {
if err := c.redis.Delete(c.keyPrefix + id); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,64 @@
package cache
import (
"encoding/json"
"voltaserve/infra"
"voltaserve/model"
"voltaserve/repo"
)
type WorkspaceCache struct {
redis *infra.RedisManager
workspaceRepo repo.WorkspaceRepo
keyPrefix string
}
func NewWorkspaceCache() *WorkspaceCache {
return &WorkspaceCache{
redis: infra.NewRedisManager(),
workspaceRepo: repo.NewWorkspaceRepo(),
keyPrefix: "workspace:",
}
}
func (c *WorkspaceCache) Set(workspace model.Workspace) error {
b, err := json.Marshal(workspace)
if err != nil {
return err
}
err = c.redis.Set(c.keyPrefix+workspace.GetID(), string(b))
if err != nil {
return err
}
return nil
}
func (c *WorkspaceCache) Get(id string) (model.Workspace, error) {
value, err := c.redis.Get(c.keyPrefix + id)
if err != nil {
return c.Refresh(id)
}
workspace := repo.NewWorkspace()
if err = json.Unmarshal([]byte(value), &workspace); err != nil {
return nil, err
}
return workspace, nil
}
func (c *WorkspaceCache) Refresh(id string) (model.Workspace, error) {
res, err := c.workspaceRepo.Find(id)
if err != nil {
return nil, err
}
if err = c.Set(res); err != nil {
return nil, err
}
return res, nil
}
func (c *WorkspaceCache) Delete(id string) error {
if err := c.redis.Delete(c.keyPrefix + id); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,47 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"voltaserve/config"
)
type ConversionClient struct {
config config.Config
}
type PipelineRunOptions struct {
FileID string `json:"fileId"`
SnapshotID string `json:"snapshotId"`
Bucket string `json:"bucket"`
Key string `json:"key"`
}
func NewConversionClient() *ConversionClient {
return &ConversionClient{
config: config.GetConfig(),
}
}
func (c *ConversionClient) RunPipeline(opts *PipelineRunOptions) error {
body, err := json.Marshal(opts)
if err != nil {
return err
}
req, err := http.NewRequest("POST", fmt.Sprintf("%s/v1/pipelines/run?api_key=%s", c.config.ConversionURL, c.config.Security.APIKey), bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return err
}
if err := res.Body.Close(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,121 @@
package config
import (
"os"
"strconv"
"strings"
)
var config *Config
func GetConfig() Config {
if config == nil {
port, err := strconv.Atoi(os.Getenv("PORT"))
if err != nil {
panic(err)
}
config = &Config{
Port: port,
}
readURLs(config)
readSecurity(config)
readS3(config)
readSearch(config)
readRedis(config)
readSMTP(config)
readLimits(config)
readDefaults(config)
}
return *config
}
func readURLs(config *Config) {
config.PublicUIURL = os.Getenv("PUBLIC_UI_URL")
config.ConversionURL = os.Getenv("CONVERSION_URL")
config.DatabaseURL = os.Getenv("POSTGRES_URL")
}
func readSecurity(config *Config) {
config.Security.JWTSigningKey = os.Getenv("SECURITY_JWT_SIGNING_KEY")
config.Security.CORSOrigins = strings.Split(os.Getenv("SECURITY_CORS_ORIGINS"), ",")
config.Security.APIKey = os.Getenv("SECURITY_API_KEY")
}
func readS3(config *Config) {
config.S3.URL = os.Getenv("S3_URL")
config.S3.AccessKey = os.Getenv("S3_ACCESS_KEY")
config.S3.SecretKey = os.Getenv("S3_SECRET_KEY")
config.S3.Region = os.Getenv("S3_REGION")
if len(os.Getenv("S3_SECURE")) > 0 {
v, err := strconv.ParseBool(os.Getenv("S3_SECURE"))
if err != nil {
panic(err)
}
config.S3.Secure = v
}
}
func readSearch(config *Config) {
config.Search.URL = os.Getenv("SEARCH_URL")
}
func readRedis(config *Config) {
config.Redis.Address = os.Getenv("REDIS_ADDRESS")
config.Redis.Password = os.Getenv("REDIS_PASSWORD")
if len(os.Getenv("REDIS_DB")) > 0 {
v, err := strconv.ParseInt(os.Getenv("REDIS_DB"), 10, 32)
if err != nil {
panic(err)
}
config.Redis.DB = int(v)
}
}
func readSMTP(config *Config) {
config.SMTP.Host = os.Getenv("SMTP_HOST")
if len(os.Getenv("SMTP_PORT")) > 0 {
v, err := strconv.ParseInt(os.Getenv("SMTP_PORT"), 10, 32)
if err != nil {
panic(err)
}
config.SMTP.Port = int(v)
}
if len(os.Getenv("SMTP_SECURE")) > 0 {
v, err := strconv.ParseBool(os.Getenv("SMTP_SECURE"))
if err != nil {
panic(err)
}
config.SMTP.Secure = v
}
config.SMTP.Username = os.Getenv("SMTP_USERNAME")
config.SMTP.Password = os.Getenv("SMTP_PASSWORD")
config.SMTP.SenderAddress = os.Getenv("SMTP_SENDER_ADDRESS")
config.SMTP.SenderName = os.Getenv("SMTP_SENDER_NAME")
}
func readLimits(config *Config) {
if len(os.Getenv("LIMITS_EXTERNAL_COMMAND_TIMEOUT_SECONDS")) > 0 {
v, err := strconv.ParseInt(os.Getenv("LIMITS_EXTERNAL_COMMAND_TIMEOUT_SECONDS"), 10, 32)
if err != nil {
panic(err)
}
config.Limits.ExternalCommandTimeoutSeconds = int(v)
}
if len(os.Getenv("LIMITS_MULTIPART_BODY_LENGTH_LIMIT_MB")) > 0 {
v, err := strconv.ParseInt(os.Getenv("LIMITS_MULTIPART_BODY_LENGTH_LIMIT_MB"), 10, 32)
if err != nil {
panic(err)
}
config.Limits.MultipartBodyLengthLimitMB = int(v)
}
}
func readDefaults(config *Config) {
if len(os.Getenv("DEFAULT_WORKSPACE_STORAGE_CAPACITY_BYTES")) > 0 {
v, err := strconv.ParseInt(os.Getenv("DEFAULT_WORKSPACE_STORAGE_CAPACITY_BYTES"), 10, 64)
if err != nil {
panic(err)
}
config.Defaults.WorkspaceStorageCapacityBytes = v
}
}

View File

@@ -0,0 +1,65 @@
package config
type Config struct {
Port int
PublicUIURL string
ConversionURL string
DatabaseURL string
Search SearchConfig
Redis RedisConfig
S3 S3Config
Limits LimitsConfig
Security SecurityConfig
SMTP SMTPConfig
Defaults DefaultsConfig
}
type LimitsConfig struct {
ExternalCommandTimeoutSeconds int
MultipartBodyLengthLimitMB int
}
type DefaultsConfig struct {
WorkspaceStorageCapacityBytes int64
}
type TokenConfig struct {
AccessTokenLifetime int
RefreshTokenLifetime int
TokenAudience string
TokenIssuer string
}
type SearchConfig struct {
URL string
}
type RedisConfig struct {
Address string
Password string
DB int
}
type S3Config struct {
URL string
AccessKey string
SecretKey string
Region string
Secure bool
}
type SecurityConfig struct {
JWTSigningKey string
CORSOrigins []string
APIKey string
}
type SMTPConfig struct {
Host string
Port int
Secure bool
Username string
Password string
SenderAddress string
SenderName string
}

763
Downloads/Voltaserve/api/docs/index.html generated Normal file

File diff suppressed because one or more lines are too long

2679
Downloads/Voltaserve/api/docs/swagger.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
package errorpkg
const (
MsgResourceNotFound = "The resource you are looking for might have been removed or temporatily unavailable."
MsgSomethingWentWrong = "Oops! something went wrong."
MsgInvalidRequest = "An invalid request was sent to the server."
)

View File

@@ -0,0 +1,415 @@
package errorpkg
import (
"fmt"
"net/http"
"strings"
"voltaserve/model"
"github.com/go-playground/validator/v10"
)
func NewGroupNotFoundError(err error) *ErrorResponse {
return NewErrorResponse(
"group_not_found",
http.StatusNotFound,
"Group not found.",
MsgResourceNotFound,
err,
)
}
func NewFileNotFoundError(err error) *ErrorResponse {
return NewErrorResponse(
"file_not_found",
http.StatusNotFound,
"File not found.",
MsgResourceNotFound,
err,
)
}
func NewInvalidPathError(err error) *ErrorResponse {
return NewErrorResponse(
"invalid_path",
http.StatusBadRequest,
"Invalid path.",
MsgInvalidRequest,
err,
)
}
func NewWorkspaceNotFoundError(err error) *ErrorResponse {
return NewErrorResponse(
"workspace_not_found",
http.StatusNotFound,
"Workspace not found.",
MsgResourceNotFound,
err,
)
}
func NewOrganizationNotFoundError(err error) *ErrorResponse {
return NewErrorResponse(
"organization_not_found",
http.StatusNotFound,
"Organization not found.",
MsgResourceNotFound,
err,
)
}
func NewSnapshotNotFoundError(err error) *ErrorResponse {
return NewErrorResponse(
"snapshot_not_found",
http.StatusNotFound,
"Snapshot not found.",
"The file has no snapshots.",
err,
)
}
func NewS3ObjectNotFoundError(err error) *ErrorResponse {
return NewErrorResponse(
"s3_object_not_found",
http.StatusNotFound,
"S3 object not found.",
"The snapshot does not contain the S3 object requested.",
err,
)
}
func NewInvitationNotFoundError(err error) *ErrorResponse {
return NewErrorResponse(
"invitation_not_found",
http.StatusNotFound,
"Invitation not found.",
MsgResourceNotFound,
err,
)
}
func NewUserNotFoundError(err error) *ErrorResponse {
return NewErrorResponse(
"user_not_found",
http.StatusNotFound,
"User not found.",
MsgResourceNotFound,
err,
)
}
func NewInternalServerError(err error) *ErrorResponse {
return NewErrorResponse(
"internal_server_error",
http.StatusInternalServerError,
"Internal server error.",
MsgSomethingWentWrong,
err,
)
}
func NewOrganizationPermissionError(user model.User, org model.Organization, permission string) *ErrorResponse {
return NewErrorResponse(
"missing_organization_permission",
http.StatusForbidden,
fmt.Sprintf(
"User '%s' (%s) is missing the permission '%s' for organization '%s' (%s).",
user.GetUsername(), user.GetID(), permission, org.GetName(), org.GetID(),
),
fmt.Sprintf("Sorry, you don't have enough permissions for organization '%s'.", org.GetName()),
nil,
)
}
func NewCannotRemoveLastRemainingOwnerOfOrganizationError(id string) *ErrorResponse {
return NewErrorResponse(
"cannot_remove_last_owner_of_organization",
http.StatusBadRequest,
fmt.Sprintf("Cannot remove the last remaining owner of organization '%s'.", id), MsgInvalidRequest,
nil,
)
}
func NewGroupPermissionError(user model.User, org model.Organization, permission string) *ErrorResponse {
return NewErrorResponse(
"missing_group_permission",
http.StatusForbidden,
fmt.Sprintf(
"User '%s' (%s) is missing the permission '%s' for the group '%s' (%s).",
user.GetUsername(), user.GetID(), permission, org.GetName(), org.GetID(),
),
fmt.Sprintf("Sorry, you don't have enough permissions for the group '%s'.", org.GetName()),
nil,
)
}
func NewWorkspacePermissionError(user model.User, workspace model.Workspace, permission string) *ErrorResponse {
return NewErrorResponse(
"missing_workspace_permission",
http.StatusForbidden,
fmt.Sprintf(
"User '%s' (%s) is missing the permission '%s' for the workspace '%s' (%s).",
user.GetUsername(), user.GetID(), permission, workspace.GetName(), workspace.GetID(),
),
fmt.Sprintf("Sorry, you don't have enough permissions for the workspace '%s'.", workspace.GetName()),
nil,
)
}
func NewFilePermissionError(user model.User, file model.File, permission string) *ErrorResponse {
return NewErrorResponse(
"missing_file_permission",
http.StatusForbidden,
fmt.Sprintf(
"User '%s' (%s) is missing the permission '%s' for the file '%s' (%s).",
user.GetUsername(), user.GetID(), permission, file.GetName(), file.GetID(),
),
fmt.Sprintf("Sorry, you don't have enough permissions for the item '%s'.", file.GetName()),
nil,
)
}
func NewS3Error(message string) *ErrorResponse {
return NewErrorResponse(
"s3_error",
http.StatusInternalServerError,
message,
MsgSomethingWentWrong,
nil,
)
}
func NewMissingQueryParamError(param string) *ErrorResponse {
return NewErrorResponse(
"missing_query_param",
http.StatusBadRequest,
fmt.Sprintf("Query param '%s' is required.", param),
MsgInvalidRequest,
nil,
)
}
func NewInvalidQueryParamError(param string) *ErrorResponse {
return NewErrorResponse(
"invalid_query_param",
http.StatusBadRequest,
fmt.Sprintf("Query param '%s' is invalid.", param),
MsgInvalidRequest,
nil,
)
}
func NewStorageLimitExceededError() *ErrorResponse {
return NewErrorResponse(
"storage_limit_exceeded",
http.StatusForbidden,
"Storage limit exceeded.",
"The storage limit of your workspace has been reached, please increase it and try again.",
nil,
)
}
func NewInsufficientStorageCapacityError() *ErrorResponse {
return NewErrorResponse(
"insufficient_storage_capacity",
http.StatusForbidden,
"Insufficient storage capacity.",
"The requested storage capacity is insufficient.",
nil,
)
}
func NewRequestBodyValidationError(err error) *ErrorResponse {
var fields []string
for _, e := range err.(validator.ValidationErrors) {
fields = append(fields, e.Field())
}
return NewErrorResponse(
"request_validation_error",
http.StatusBadRequest,
fmt.Sprintf("Failed validation for the following fields: %s.", strings.Join(fields, ",")),
MsgInvalidRequest,
err,
)
}
func NewFileAlreadyChildOfDestinationError(source model.File, target model.File) *ErrorResponse {
return NewErrorResponse(
"file_already_child_of_destination",
http.StatusForbidden,
fmt.Sprintf("File '%s' (%s) is already a child of '%s' (%s).", source.GetName(), source.GetID(), target.GetName(), target.GetID()),
fmt.Sprintf("Item '%s' is already within '%s'.", source.GetName(), target.GetName()),
nil,
)
}
func NewFileCannotBeMovedIntoItselfError(source model.File) *ErrorResponse {
return NewErrorResponse(
"file_cannot_be_moved_into_itself",
http.StatusForbidden,
fmt.Sprintf("File '%s' (%s) cannot be moved into itself.", source.GetName(), source.GetID()),
fmt.Sprintf("Item '%s' cannot be moved into itself.", source.GetName()),
nil,
)
}
func NewFileIsNotAFolderError(file model.File) *ErrorResponse {
return NewErrorResponse(
"file_is_not_a_folder",
http.StatusForbidden,
fmt.Sprintf("File '%s' (%s) is not a folder.", file.GetName(), file.GetID()),
fmt.Sprintf("Item '%s' is not a folder.", file.GetName()),
nil,
)
}
func NewTargetIsGrandChildOfSourceError(file model.File) *ErrorResponse {
return NewErrorResponse(
"target_is_grant_child_of_source",
http.StatusForbidden,
fmt.Sprintf("File '%s' (%s) cannot be moved in another file within its own tree.", file.GetName(), file.GetID()),
fmt.Sprintf("Item '%s' cannot be moved in another item within its own tree.", file.GetName()),
nil,
)
}
func NewCannotDeleteWorkspaceRootError(file model.File, workspace model.Workspace) *ErrorResponse {
return NewErrorResponse(
"cannot_delete_workspace_root",
http.StatusForbidden,
fmt.Sprintf("Cannot delete the root file (%s) of the workspace '%s' (%s).", file.GetID(), workspace.GetName(), workspace.GetID()),
fmt.Sprintf("Cannot delete the root item of the workspace '%s'.", workspace.GetName()),
nil,
)
}
func NewFileCannotBeCopiedIntoOwnSubtreeError(file model.File) *ErrorResponse {
return NewErrorResponse(
"file_cannot_be_coped_into_own_subtree",
http.StatusForbidden,
fmt.Sprintf("File '%s' (%s) cannot be copied in another file within its own subtree.", file.GetName(), file.GetID()),
fmt.Sprintf("Item '%s' cannot be copied in another item within its own subtree.", file.GetName()),
nil,
)
}
func NewFileCannotBeCopiedIntoIselfError(file model.File) *ErrorResponse {
return NewErrorResponse(
"file_cannot_be_copied_into_itself",
http.StatusForbidden,
fmt.Sprintf("File '%s' (%s) cannot be copied into itself.", file.GetName(), file.GetID()),
fmt.Sprintf("Item '%s' cannot be copied into itself.", file.GetName()),
nil,
)
}
func NewFileWithSimilarNameExistsError() *ErrorResponse {
return NewErrorResponse(
"file_with_similar_name_exists",
http.StatusForbidden,
"File with similar name exists.",
"Item with similar name exists.",
nil,
)
}
func NewInvalidPageParameterError() *ErrorResponse {
return NewErrorResponse(
"invalid_page_parameter",
http.StatusBadRequest,
"Invalid page parameter, must be >= 1.",
MsgInvalidRequest,
nil,
)
}
func NewInvalidSizeParameterError() *ErrorResponse {
return NewErrorResponse(
"invalid_size_parameter",
http.StatusBadRequest,
"Invalid size parameter, must be >= 1.",
MsgInvalidRequest,
nil,
)
}
func NewCannotAcceptNonPendingInvitationError(invitation model.Invitation) *ErrorResponse {
return NewErrorResponse(
"cannot_accept_non_pending_invitation",
http.StatusForbidden,
fmt.Sprintf("Cannot accept an invitation which is not pending, the status of the invitation (%s) is (%s).", invitation.GetID(), invitation.GetStatus()),
"Cannot accept an invitation which is not pending.",
nil,
)
}
func NewCannotDeclineNonPendingInvitationError(invitation model.Invitation) *ErrorResponse {
return NewErrorResponse(
"cannot_decline_non_pending_invitation",
http.StatusForbidden,
fmt.Sprintf("Cannot decline an invitation which is not pending, the status of the invitation (%s) is (%s).", invitation.GetID(), invitation.GetStatus()),
"Cannot decline an invitation which is not pending.",
nil,
)
}
func NewCannotResendNonPendingInvitationError(invitation model.Invitation) *ErrorResponse {
return NewErrorResponse(
"cannot_resend_non_pending_invitation",
http.StatusForbidden,
fmt.Sprintf("Cannot resend an invitation which is not pending, the status of the invitation (%s) is (%s).", invitation.GetID(), invitation.GetStatus()),
"Cannot resend an invitation which is not pending.",
nil,
)
}
func NewUserNotAllowedToAcceptInvitationError(user model.User, invitation model.Invitation) *ErrorResponse {
return NewErrorResponse(
"user_not_allowed_to_accept_invitation",
http.StatusForbidden,
fmt.Sprintf("User '%s' (%s) is not allowed to accept the invitation (%s).", user.GetUsername(), user.GetID(), invitation.GetID()),
"Not allowed to accept this invitation.",
nil,
)
}
func NewUserNotAllowedToDeclineInvitationError(user model.User, invitation model.Invitation) *ErrorResponse {
return NewErrorResponse(
"user_not_allowed_to_decline_invitation",
http.StatusForbidden,
fmt.Sprintf("User '%s' (%s) is not allowed to decline the invitation (%s).", user.GetUsername(), user.GetID(), invitation.GetID()),
"Not allowed to decline this invitation.",
nil,
)
}
func NewUserNotAllowedToDeleteInvitationError(user model.User, invitation model.Invitation) *ErrorResponse {
return NewErrorResponse(
"user_not_allowed_to_delete_invitation",
http.StatusForbidden,
fmt.Sprintf("User '%s' (%s) not allowed to delete the invitation (%s).", user.GetUsername(), user.GetID(), invitation.GetID()),
"Not allowed to delete this invitation.",
nil,
)
}
func NewUserAlreadyMemberOfOrganizationError(user model.User, org model.Organization) *ErrorResponse {
return NewErrorResponse(
"user_already_member_of_organization",
http.StatusForbidden,
fmt.Sprintf("User '%s' (%s) is already a member of the organization '%s' (%s).", user.GetUsername(), user.GetID(), org.GetName(), org.GetID()),
fmt.Sprintf("You are already a member of the organization '%s'.", org.GetName()),
nil,
)
}
func NewInvalidAPIKeyError() *ErrorResponse {
return NewErrorResponse(
"invalid_api_key",
http.StatusUnauthorized,
"Invalid API key.",
"The API key is either missing or invalid.",
nil,
)
}

View File

@@ -0,0 +1,20 @@
package errorpkg
import (
"errors"
"net/http"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
)
func ErrorHandler(c *fiber.Ctx, err error) error {
var e *ErrorResponse
if errors.As(err, &e) {
v := err.(*ErrorResponse)
return c.Status(v.Status).JSON(v)
} else {
log.Error(err)
return c.Status(http.StatusInternalServerError).JSON(NewInternalServerError(err))
}
}

View File

@@ -0,0 +1,31 @@
package errorpkg
import "fmt"
type ErrorResponse struct {
Code string `json:"code"`
Status int `json:"status"`
Message string `json:"message"`
UserMessage string `json:"userMessage"`
MoreInfo string `json:"moreInfo"`
Err error `json:"-"`
}
func NewErrorResponse(code string, status int, message string, userMessage string, err error) *ErrorResponse {
return &ErrorResponse{
Code: code,
Status: status,
Message: message,
UserMessage: userMessage,
MoreInfo: fmt.Sprintf("https://voltaserve.com/docs/api/errors/%s", code),
Err: err,
}
}
func (err ErrorResponse) Error() string {
return fmt.Sprintf("%s %s", err.Code, err.Message)
}
func (err ErrorResponse) Unwrap() error {
return err.Err
}

View File

@@ -0,0 +1,82 @@
module voltaserve
go 1.21
toolchain go1.22.2
require (
github.com/gabriel-vasile/mimetype v1.4.3
github.com/go-playground/validator/v10 v10.19.0
github.com/gofiber/contrib/jwt v1.0.8
github.com/gofiber/fiber/v2 v2.52.4
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/gosimple/slug v1.14.0
github.com/joho/godotenv v1.5.1
github.com/meilisearch/meilisearch-go v0.26.2
github.com/minio/minio-go/v7 v7.0.69
github.com/reactivex/rxgo/v2 v2.5.0
github.com/redis/go-redis/v9 v9.5.1
github.com/speps/go-hashids/v2 v2.0.1
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gorm.io/datatypes v1.2.0
gorm.io/driver/postgres v1.5.7
gorm.io/gorm v1.25.9
sigs.k8s.io/yaml v1.4.0
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/teivah/onecontext v1.3.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.52.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.14.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
)

View File

@@ -0,0 +1,228 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gofiber/contrib/jwt v1.0.8 h1:/GeOsm/Mr1OGr0GTy+RIVSz5VgNNyP3ZgK4wdqxF/WY=
github.com/gofiber/contrib/jwt v1.0.8/go.mod h1:gWWBtBiLmKXRN7xy6a96QO0KGvPEyxdh8x496Ujtg84=
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gosimple/slug v1.14.0 h1:RtTL/71mJNDfpUbCOmnf/XFkzKRtD6wL6Uy+3akm4Es=
github.com/gosimple/slug v1.14.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/meilisearch/meilisearch-go v0.26.2 h1:3gTlmiV1dHHumVUhYdJbvh3camiNiyqQ1hNveVsU2OE=
github.com/meilisearch/meilisearch-go v0.26.2/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0=
github.com/minio/minio-go/v7 v7.0.69/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/reactivex/rxgo/v2 v2.5.0 h1:FhPgHwX9vKdNQB2gq9EPt+EKk9QrrzoeztGbEEnZam4=
github.com/reactivex/rxgo/v2 v2.5.0/go.mod h1:bs4fVZxcb5ZckLIOeIeVH942yunJLWDABWGbrHAW+qU=
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/speps/go-hashids/v2 v2.0.1 h1:ViWOEqWES/pdOSq+C1SLVa8/Tnsd52XC34RY7lt7m4g=
github.com/speps/go-hashids/v2 v2.0.1/go.mod h1:47LKunwvDZki/uRVD6NImtyk712yFzIs3UF3KlHohGw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/teivah/onecontext v0.0.0-20200513185103-40f981bfd775/go.mod h1:XUZ4x3oGhWfiOnUvTslnKKs39AWUct3g3yJvXTQSJOQ=
github.com/teivah/onecontext v1.3.0 h1:tbikMhAlo6VhAuEGCvhc8HlTnpX4xTNPTOseWuhO1J0=
github.com/teivah/onecontext v1.3.0/go.mod h1:hoW1nmdPVK/0jrvGtcx8sCKYs2PiS4z0zzfdeuEVyb0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco=
gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

View File

@@ -0,0 +1,52 @@
package guard
import (
"voltaserve/cache"
"voltaserve/errorpkg"
"voltaserve/model"
"github.com/gofiber/fiber/v2/log"
)
type FileGuard struct {
groupCache *cache.GroupCache
}
func NewFileGuard() *FileGuard {
return &FileGuard{
groupCache: cache.NewGroupCache(),
}
}
func (g *FileGuard) IsAuthorized(user model.User, file model.File, permission string) bool {
for _, p := range file.GetUserPermissions() {
if p.GetUserID() == user.GetID() && model.IsEquivalentPermission(p.GetValue(), permission) {
return true
}
}
for _, p := range file.GetGroupPermissions() {
g, err := g.groupCache.Get(p.GetGroupID())
if err != nil {
log.Error(err)
return false
}
for _, u := range g.GetUsers() {
if u == user.GetID() && model.IsEquivalentPermission(p.GetValue(), permission) {
return true
}
}
}
return false
}
func (g *FileGuard) Authorize(user model.User, file model.File, permission string) error {
if !g.IsAuthorized(user, file, permission) {
err := errorpkg.NewFilePermissionError(user, file, permission)
if g.IsAuthorized(user, file, model.PermissionViewer) {
return err
} else {
return errorpkg.NewOrganizationNotFoundError(err)
}
}
return nil
}

View File

@@ -0,0 +1,52 @@
package guard
import (
"voltaserve/cache"
"voltaserve/errorpkg"
"voltaserve/model"
"github.com/gofiber/fiber/v2/log"
)
type GroupGuard struct {
groupCache *cache.GroupCache
}
func NewGroupGuard() *GroupGuard {
return &GroupGuard{
groupCache: cache.NewGroupCache(),
}
}
func (g *GroupGuard) IsAuthorized(user model.User, group model.Group, permission string) bool {
for _, p := range group.GetUserPermissions() {
if p.GetUserID() == user.GetID() && model.IsEquivalentPermission(p.GetValue(), permission) {
return true
}
}
for _, p := range group.GetGroupPermissions() {
g, err := g.groupCache.Get(p.GetGroupID())
if err != nil {
log.Error(err)
return false
}
for _, u := range g.GetUsers() {
if u == user.GetID() && model.IsEquivalentPermission(p.GetValue(), permission) {
return true
}
}
}
return false
}
func (g *GroupGuard) Authorize(user model.User, group model.Group, permission string) error {
if !g.IsAuthorized(user, group, permission) {
err := errorpkg.NewGroupPermissionError(user, group, permission)
if g.IsAuthorized(user, group, model.PermissionViewer) {
return err
} else {
return errorpkg.NewGroupNotFoundError(err)
}
}
return nil
}

View File

@@ -0,0 +1,52 @@
package guard
import (
"voltaserve/cache"
"voltaserve/errorpkg"
"voltaserve/model"
"github.com/gofiber/fiber/v2/log"
)
type OrganizationGuard struct {
groupCache *cache.GroupCache
}
func NewOrganizationGuard() *OrganizationGuard {
return &OrganizationGuard{
groupCache: cache.NewGroupCache(),
}
}
func (g *OrganizationGuard) IsAuthorized(user model.User, org model.Organization, permission string) bool {
for _, p := range org.GetUserPermissions() {
if p.GetUserID() == user.GetID() && model.IsEquivalentPermission(p.GetValue(), permission) {
return true
}
}
for _, p := range org.GetGroupPermissions() {
g, err := g.groupCache.Get(p.GetGroupID())
if err != nil {
log.Error(err)
return false
}
for _, u := range g.GetUsers() {
if u == user.GetID() && model.IsEquivalentPermission(p.GetValue(), permission) {
return true
}
}
}
return false
}
func (g *OrganizationGuard) Authorize(user model.User, org model.Organization, permission string) error {
if !g.IsAuthorized(user, org, permission) {
err := errorpkg.NewOrganizationPermissionError(user, org, permission)
if g.IsAuthorized(user, org, model.PermissionViewer) {
return err
} else {
return errorpkg.NewOrganizationNotFoundError(err)
}
}
return nil
}

View File

@@ -0,0 +1,52 @@
package guard
import (
"voltaserve/cache"
"voltaserve/errorpkg"
"voltaserve/model"
"github.com/gofiber/fiber/v2/log"
)
type WorkspaceGuard struct {
groupCache *cache.GroupCache
}
func NewWorkspaceGuard() *WorkspaceGuard {
return &WorkspaceGuard{
groupCache: cache.NewGroupCache(),
}
}
func (g *WorkspaceGuard) IsAuthorized(user model.User, workspace model.Workspace, permission string) bool {
for _, p := range workspace.GetUserPermissions() {
if p.GetUserID() == user.GetID() && model.IsEquivalentPermission(p.GetValue(), permission) {
return true
}
}
for _, p := range workspace.GetGroupPermissions() {
g, err := g.groupCache.Get(p.GetGroupID())
if err != nil {
log.Error(err)
return false
}
for _, u := range g.GetUsers() {
if u == user.GetID() && model.IsEquivalentPermission(p.GetValue(), permission) {
return true
}
}
}
return false
}
func (g *WorkspaceGuard) Authorize(user model.User, workspace model.Workspace, permission string) error {
if !g.IsAuthorized(user, workspace, permission) {
err := errorpkg.NewWorkspacePermissionError(user, workspace, permission)
if g.IsAuthorized(user, workspace, model.PermissionViewer) {
return err
} else {
return errorpkg.NewWorkspaceNotFoundError(err)
}
}
return nil
}

View File

@@ -0,0 +1,22 @@
package helper
import (
"time"
"github.com/google/uuid"
"github.com/speps/go-hashids/v2"
)
func NewID() string {
hd := hashids.NewData()
hd.Salt = uuid.NewString()
h, err := hashids.NewWithData(hd)
if err != nil {
panic(err)
}
id, err := h.EncodeInt64([]int64{time.Now().UTC().UnixNano()})
if err != nil {
panic(err)
}
return id
}

View File

@@ -0,0 +1,5 @@
package helper
func MegabyteToByte(mb int) int64 {
return int64(mb) * 1000000
}

View File

@@ -0,0 +1,17 @@
package helper
import (
"fmt"
"strings"
"github.com/gosimple/slug"
)
func SlugFromWorkspace(id string, name string) string {
return fmt.Sprintf("%s-%s", slug.Make(name), id)
}
func WorkspaceIDFromSlug(slug string) string {
parts := strings.Split(slug, "-")
return parts[len(parts)-1]
}

View File

@@ -0,0 +1,22 @@
package infra
import (
"voltaserve/config"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var db *gorm.DB
func GetDb() *gorm.DB {
if db != nil {
return db
}
var err error
db, err = gorm.Open(postgres.Open(config.GetConfig().DatabaseURL), &gorm.Config{})
if err != nil {
panic(err)
}
return db
}

View File

@@ -0,0 +1,123 @@
package infra
import "strings"
type FileIdentifier struct {
}
func NewFileIdentifier() *FileIdentifier {
return &FileIdentifier{}
}
func (fi *FileIdentifier) IsPDF(extension string) bool {
return strings.ToLower(extension) == ".pdf"
}
func (fi *FileIdentifier) IsOffice(extension string) bool {
extensions := []string{
".xls",
".doc",
".ppt",
".xlsx",
".docx",
".pptx",
".odt",
".ott",
".ods",
".ots",
".odp",
".otp",
".odg",
".otg",
".odf",
".odc",
".rtf",
}
for _, v := range extensions {
if strings.ToLower(extension) == v {
return true
}
}
return false
}
func (fi *FileIdentifier) IsPlainText(extension string) bool {
extensions := []string{
".txt",
".html",
".js",
"jsx",
".ts",
".tsx",
".css",
".sass",
".scss",
".go",
".py",
".rb",
".java",
".c",
".h",
".cpp",
".hpp",
".json",
".yml",
".yaml",
".toml",
".md",
}
for _, v := range extensions {
if strings.ToLower(extension) == v {
return true
}
}
return false
}
func (fi *FileIdentifier) IsImage(extension string) bool {
extensions := []string{
".xpm",
".png",
".jpg",
".jpeg",
".jp2",
".gif",
".webp",
".tiff",
".bmp",
".ico",
".heif",
".xcf",
".svg",
}
for _, v := range extensions {
if strings.ToLower(extension) == v {
return true
}
}
return false
}
func (fi *FileIdentifier) IsVideo(extension string) bool {
extensions := []string{
".ogv",
".mpeg",
".mov",
".mqv",
".mp4",
".webm",
".3gp",
".3g2",
".avi",
".flv",
".mkv",
".asf",
".m4v",
}
for _, v := range extensions {
if strings.ToLower(extension) == v {
return true
}
}
return false
}

View File

@@ -0,0 +1,99 @@
package infra
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"voltaserve/config"
"github.com/gofiber/fiber/v2/log"
"gopkg.in/gomail.v2"
"sigs.k8s.io/yaml"
"text/template"
)
type MessageParams struct {
Subject string
}
type MailTemplate struct {
dialer *gomail.Dialer
config config.SMTPConfig
}
func NewMailTemplate() *MailTemplate {
mt := new(MailTemplate)
mt.config = config.GetConfig().SMTP
mt.dialer = gomail.NewDialer(mt.config.Host, mt.config.Port, mt.config.Username, mt.config.Password)
return mt
}
func (mt *MailTemplate) Send(templateName string, address string, variables map[string]string) error {
html, err := mt.GetText(filepath.FromSlash("templates/"+templateName+"/template.html"), variables)
if err != nil {
return err
}
text, err := mt.GetText(filepath.FromSlash("templates/"+templateName+"/template.txt"), variables)
if err != nil {
return err
}
params, err := mt.GetMessageParams(templateName)
if err != nil {
return err
}
m := gomail.NewMessage()
m.SetHeader("From", fmt.Sprintf(`"%s" <%s>`, mt.config.SenderName, mt.config.SenderAddress))
m.SetHeader("To", address)
m.SetHeader("Subject", params.Subject)
m.SetBody("text/plain ", text)
m.AddAlternative("text/html", html)
if err := mt.dialer.DialAndSend(m); err != nil {
return err
}
return nil
}
func (mt *MailTemplate) GetText(path string, variables map[string]string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer func(f *os.File) {
if err := f.Close(); err != nil {
log.Error(err)
}
}(f)
b, _ := io.ReadAll(f)
html := string(b)
tmpl, err := template.New("").Parse(html)
if err != nil {
return "", nil
}
var buf bytes.Buffer
err = tmpl.Execute(&buf, variables)
if err != nil {
return "", nil
}
return buf.String(), nil
}
func (mt *MailTemplate) GetMessageParams(templateName string) (*MessageParams, error) {
f, err := os.Open(filepath.FromSlash("templates/" + templateName + "/params.yml"))
if err != nil {
return nil, err
}
defer func(f *os.File) {
if err := f.Close(); err != nil {
log.Error(err)
}
}(f)
b, _ := io.ReadAll(f)
res := &MessageParams{}
if err := yaml.Unmarshal(b, res); err != nil {
return nil, err
}
return res, nil
}

View File

@@ -0,0 +1,15 @@
package infra
import "github.com/gabriel-vasile/mimetype"
func DetectMimeFromFile(path string) string {
mime, err := mimetype.DetectFile(path)
if err != nil {
return "application/octet-stream"
}
return mime.String()
}
func DetectMimeFromBytes(b []byte) string {
return mimetype.Detect(b).String()
}

View File

@@ -0,0 +1,113 @@
package infra
import (
"context"
"strings"
"voltaserve/config"
"github.com/redis/go-redis/v9"
)
type RedisManager struct {
config config.RedisConfig
client *redis.Client
clusterClient *redis.ClusterClient
}
func NewRedisManager() *RedisManager {
mgr := new(RedisManager)
mgr.config = config.GetConfig().Redis
return mgr
}
func (mgr *RedisManager) Set(key string, value interface{}) error {
if mgr.client == nil && mgr.clusterClient == nil {
if err := mgr.connect(); err != nil {
return err
}
}
if mgr.clusterClient != nil {
if _, err := mgr.clusterClient.Set(context.Background(), key, value, 0).Result(); err != nil {
return err
}
} else {
if _, err := mgr.client.Set(context.Background(), key, value, 0).Result(); err != nil {
return err
}
}
return nil
}
func (mgr *RedisManager) Get(key string) (string, error) {
if mgr.client == nil && mgr.clusterClient == nil {
if err := mgr.connect(); err != nil {
return "", err
}
}
if mgr.clusterClient != nil {
value, err := mgr.clusterClient.Get(context.Background(), key).Result()
if err != nil {
return "", err
}
return value, nil
} else {
value, err := mgr.client.Get(context.Background(), key).Result()
if err != nil {
return "", err
}
return value, nil
}
}
func (mgr *RedisManager) Delete(key string) error {
if mgr.client == nil && mgr.clusterClient == nil {
if err := mgr.connect(); err != nil {
return err
}
}
if mgr.clusterClient != nil {
if _, err := mgr.clusterClient.Del(context.Background(), key).Result(); err != nil {
return err
}
} else {
if _, err := mgr.client.Del(context.Background(), key).Result(); err != nil {
return err
}
}
return nil
}
func (mgr *RedisManager) Close() error {
if mgr.client != nil {
if err := mgr.client.Close(); err != nil {
return err
}
}
return nil
}
func (mgr *RedisManager) connect() error {
if mgr.client != nil || mgr.clusterClient != nil {
return nil
}
addresses := strings.Split(mgr.config.Address, ";")
if len(addresses) > 1 {
mgr.clusterClient = redis.NewClusterClient(&redis.ClusterOptions{
Addrs: addresses,
Password: mgr.config.Password,
})
if err := mgr.clusterClient.Ping(context.Background()).Err(); err != nil {
return err
}
} else {
mgr.client = redis.NewClient(&redis.Options{
Addr: mgr.config.Address,
Password: mgr.config.Password,
DB: mgr.config.DB,
})
if err := mgr.client.Ping(context.Background()).Err(); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,205 @@
package infra
import (
"bytes"
"context"
"io"
"strings"
"voltaserve/config"
"voltaserve/errorpkg"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/minio-go/v7/pkg/sse"
)
type S3Manager struct {
config config.S3Config
client *minio.Client
}
func NewS3Manager() *S3Manager {
mgr := new(S3Manager)
mgr.config = config.GetConfig().S3
return mgr
}
func (mgr *S3Manager) GetFile(objectName string, filePath string, bucketName string) error {
if mgr.client == nil {
if err := mgr.connect(); err != nil {
return err
}
}
if err := mgr.client.FGetObject(context.Background(), bucketName, objectName, filePath, minio.GetObjectOptions{}); err != nil {
return err
}
return nil
}
func (mgr *S3Manager) PutFile(objectName string, filePath string, contentType string, bucketName string) error {
if mgr.client == nil {
if err := mgr.connect(); err != nil {
return err
}
}
if contentType == "" {
contentType = "application/octet-stream"
}
if _, err := mgr.client.FPutObject(context.Background(), bucketName, objectName, filePath, minio.PutObjectOptions{
ContentType: contentType,
}); err != nil {
return err
}
return nil
}
func (mgr *S3Manager) PutText(objectName string, text string, contentType string, bucketName string) error {
if contentType != "" && contentType != "text/plain" && contentType != "application/json" {
return errorpkg.NewS3Error("Invalid content type '" + contentType + "'")
}
if contentType == "" {
contentType = "text/plain"
}
if mgr.client == nil {
if err := mgr.connect(); err != nil {
return err
}
}
if _, err := mgr.client.PutObject(context.Background(), bucketName, objectName, strings.NewReader(text), int64(len(text)), minio.PutObjectOptions{
ContentType: contentType,
}); err != nil {
return err
}
return nil
}
func (mgr *S3Manager) GetObject(objectName string, bucketName string) (*bytes.Buffer, error) {
if mgr.client == nil {
if err := mgr.connect(); err != nil {
return nil, err
}
}
reader, err := mgr.client.GetObject(context.Background(), bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
return nil, err
}
var buf bytes.Buffer
_, err = io.Copy(io.Writer(&buf), reader)
if err != nil {
return nil, nil
}
return &buf, nil
}
func (mgr *S3Manager) GetText(objectName string, bucketName string) (string, error) {
if mgr.client == nil {
if err := mgr.connect(); err != nil {
return "", err
}
}
reader, err := mgr.client.GetObject(context.Background(), bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
return "", err
}
buf := new(strings.Builder)
_, err = io.Copy(buf, reader)
if err != nil {
return "", nil
}
return buf.String(), nil
}
func (mgr *S3Manager) RemoveObject(objectName string, bucketName string) error {
if mgr.client == nil {
if err := mgr.connect(); err != nil {
return err
}
}
err := mgr.client.RemoveObject(context.Background(), bucketName, objectName, minio.RemoveObjectOptions{})
if err != nil {
return err
}
return nil
}
func (mgr *S3Manager) CreateBucket(bucketName string) error {
if mgr.client == nil {
if err := mgr.connect(); err != nil {
return err
}
}
found, err := mgr.client.BucketExists(context.Background(), bucketName)
if err != nil {
return err
}
if !found {
if err = mgr.client.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{
Region: mgr.config.Region,
}); err != nil {
return err
}
}
return nil
}
func (mgr *S3Manager) RemoveBucket(bucketName string) error {
if mgr.client == nil {
if err := mgr.connect(); err != nil {
return err
}
}
found, err := mgr.client.BucketExists(context.Background(), bucketName)
if err != nil {
return err
}
if !found {
return nil
}
objectCh := mgr.client.ListObjects(context.Background(), bucketName, minio.ListObjectsOptions{
Prefix: "",
Recursive: true,
})
mgr.client.RemoveObjects(context.Background(), bucketName, objectCh, minio.RemoveObjectsOptions{})
if err = mgr.client.RemoveBucket(context.Background(), bucketName); err != nil {
return err
}
return nil
}
func (mgr *S3Manager) EnableBucketEncryption(bucketName string) error {
if mgr.client == nil {
if err := mgr.connect(); err != nil {
return err
}
}
err := mgr.client.SetBucketEncryption(context.Background(), bucketName, sse.NewConfigurationSSES3())
if err != nil {
return err
}
return nil
}
func (mgr *S3Manager) DisableBucketEncryption(bucketName string) error {
if mgr.client == nil {
if err := mgr.connect(); err != nil {
return err
}
}
err := mgr.client.RemoveBucketEncryption(context.Background(), bucketName)
if err != nil {
return err
}
return nil
}
func (mgr *S3Manager) connect() error {
client, err := minio.New(mgr.config.URL, &minio.Options{
Creds: credentials.NewStaticV4(mgr.config.AccessKey, mgr.config.SecretKey, ""),
Secure: mgr.config.Secure,
})
if err != nil {
return err
}
mgr.client = client
return nil
}

View File

@@ -0,0 +1,123 @@
package infra
import (
"voltaserve/config"
"github.com/meilisearch/meilisearch-go"
)
var searchClient *meilisearch.Client
const (
FileSearchIndex = "file"
GroupSearchIndex = "group"
WorkspaceSearchIndex = "workspace"
OrganizationSearchIndex = "organization"
UserSearchIndex = "user"
)
type SearchModel interface {
GetID() string
}
type SearchManager struct {
config config.SearchConfig
}
func NewSearchManager() *SearchManager {
if searchClient == nil {
searchClient = meilisearch.NewClient(meilisearch.ClientConfig{
Host: config.GetConfig().Search.URL,
})
if _, err := searchClient.CreateIndex(&meilisearch.IndexConfig{
Uid: FileSearchIndex,
PrimaryKey: "id",
}); err != nil {
panic(err)
}
if _, err := searchClient.Index(FileSearchIndex).UpdateSettings(&meilisearch.Settings{
SearchableAttributes: []string{"name", "text"},
}); err != nil {
panic(err)
}
if _, err := searchClient.CreateIndex(&meilisearch.IndexConfig{
Uid: GroupSearchIndex,
PrimaryKey: "id",
}); err != nil {
panic(err)
}
if _, err := searchClient.Index(GroupSearchIndex).UpdateSettings(&meilisearch.Settings{
SearchableAttributes: []string{"name"},
}); err != nil {
panic(err)
}
if _, err := searchClient.CreateIndex(&meilisearch.IndexConfig{
Uid: WorkspaceSearchIndex,
PrimaryKey: "id",
}); err != nil {
panic(err)
}
if _, err := searchClient.Index(WorkspaceSearchIndex).UpdateSettings(&meilisearch.Settings{
SearchableAttributes: []string{"name"},
}); err != nil {
panic(err)
}
if _, err := searchClient.CreateIndex(&meilisearch.IndexConfig{
Uid: OrganizationSearchIndex,
PrimaryKey: "id",
}); err != nil {
panic(err)
}
if _, err := searchClient.Index(OrganizationSearchIndex).UpdateSettings(&meilisearch.Settings{
SearchableAttributes: []string{"name"},
}); err != nil {
panic(err)
}
if _, err := searchClient.CreateIndex(&meilisearch.IndexConfig{
Uid: UserSearchIndex,
PrimaryKey: "id",
}); err != nil {
panic(err)
}
if _, err := searchClient.Index(UserSearchIndex).UpdateSettings(&meilisearch.Settings{
SearchableAttributes: []string{"fullName", "email"},
}); err != nil {
panic(err)
}
}
return &SearchManager{
config: config.GetConfig().Search,
}
}
func (mgr *SearchManager) Query(index string, query string) ([]interface{}, error) {
res, err := searchClient.Index(index).Search(query, &meilisearch.SearchRequest{})
if err != nil {
return nil, err
}
return res.Hits, nil
}
func (mgr *SearchManager) Index(index string, models []SearchModel) error {
_, err := searchClient.Index(index).AddDocuments(models)
if err != nil {
return err
}
return nil
}
func (mgr *SearchManager) Update(index string, m []SearchModel) error {
_, err := searchClient.Index(index).UpdateDocuments(m)
if err != nil {
return err
}
return nil
}
func (mgr *SearchManager) Delete(index string, ids []string) error {
_, err := searchClient.Index(index).DeleteDocuments(ids)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,92 @@
package main
import (
"fmt"
"os"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"voltaserve/config"
"voltaserve/errorpkg"
"voltaserve/helper"
"voltaserve/router"
jwtware "github.com/gofiber/contrib/jwt"
"github.com/joho/godotenv"
)
// @title Voltaserve API
// @version 1.0.0
// @BasePath /v1
func main() {
if _, err := os.Stat(".env.local"); err == nil {
err := godotenv.Load(".env.local")
if err != nil {
panic(err)
}
} else {
err := godotenv.Load()
if err != nil {
panic(err)
}
}
cfg := config.GetConfig()
app := fiber.New(fiber.Config{
ErrorHandler: errorpkg.ErrorHandler,
BodyLimit: int(helper.MegabyteToByte(cfg.Limits.MultipartBodyLengthLimitMB)),
})
v1 := app.Group("/v1")
f := v1.Group("files")
app.Get("v1/health", func(c *fiber.Ctx) error {
return c.SendStatus(200)
})
app.Use(cors.New(cors.Config{
AllowOrigins: strings.Join(cfg.Security.CORSOrigins, ","),
}))
fileDownloads := router.NewFileDownloadRouter()
fileDownloads.AppendNonJWTRoutes(f)
conversionWebhook := router.NewConversionWebhookRouter()
conversionWebhook.AppendInternalRoutes(f)
app.Use(jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{Key: []byte(cfg.Security.JWTSigningKey)},
}))
files := router.NewFileRouter()
files.AppendRoutes(f)
invitations := router.NewInvitationRouter()
invitations.AppendRoutes(v1.Group("invitations"))
notifications := router.NewNotificationRouter()
notifications.AppendRoutes(v1.Group("notifications"))
organizations := router.NewOrganizationRouter()
organizations.AppendRoutes(v1.Group("organizations"))
storage := router.NewStorageRouter()
storage.AppendRoutes(v1.Group("storage"))
workspaces := router.NewWorkspaceRouter()
workspaces.AppendRoutes(v1.Group("workspaces"))
groups := router.NewGroupRouter()
groups.AppendRoutes(v1.Group("groups"))
users := router.NewUserRouter()
users.AppendRoutes(v1.Group("users"))
if err := app.Listen(fmt.Sprintf(":%d", cfg.Port)); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,28 @@
package model
const (
FileTypeFile = "file"
FileTypeFolder = "folder"
)
type File interface {
GetID() string
GetWorkspaceID() string
GetName() string
GetType() string
GetParentID() *string
GetCreateTime() string
GetUpdateTime() *string
GetSnapshots() []Snapshot
GetUserPermissions() []CoreUserPermission
GetGroupPermissions() []CoreGroupPermission
GetText() *string
SetID(string)
SetParentID(*string)
SetWorkspaceID(string)
SetType(string)
SetName(string)
SetText(*string)
SetCreateTime(string)
SetUpdateTime(*string)
}

View File

@@ -0,0 +1,14 @@
package model
type Group interface {
GetID() string
GetName() string
GetOrganizationID() string
GetUserPermissions() []CoreUserPermission
GetGroupPermissions() []CoreGroupPermission
GetUsers() []string
GetCreateTime() string
GetUpdateTime() *string
SetName(string)
SetUpdateTime(*string)
}

View File

@@ -0,0 +1,19 @@
package model
const (
InvitationStatusPending = "pending"
InvitationStatusAccepted = "accepted"
InvitationStatusDeclined = "declined"
)
type Invitation interface {
GetID() string
GetOrganizationID() string
GetOwnerID() string
GetEmail() string
GetStatus() string
GetCreateTime() string
GetUpdateTime() *string
SetStatus(string)
SetUpdateTime(*string)
}

View File

@@ -0,0 +1,13 @@
package model
type Organization interface {
GetID() string
GetName() string
GetUserPermissions() []CoreUserPermission
GetGroupPermissions() []CoreGroupPermission
GetUsers() []string
GetCreateTime() string
GetUpdateTime() *string
SetName(string)
SetUpdateTime(*string)
}

View File

@@ -0,0 +1,58 @@
package model
const (
PermissionViewer = "viewer"
PermissionEditor = "editor"
PermissionOwner = "owner"
)
type CoreUserPermission interface {
GetUserID() string
GetValue() string
}
type CoreGroupPermission interface {
GetGroupID() string
GetValue() string
}
func GteViewerPermission(permission string) bool {
return permission == PermissionViewer || permission == PermissionEditor || permission == PermissionOwner
}
func GteEditorPermission(permission string) bool {
return permission == PermissionEditor || permission == PermissionOwner
}
func GteOwnerPermission(permission string) bool {
return permission == PermissionOwner
}
func IsEquivalentPermission(permission string, otherPermission string) bool {
if permission == otherPermission {
return true
}
if otherPermission == PermissionViewer && GteViewerPermission(permission) {
return true
}
if otherPermission == PermissionEditor && GteEditorPermission(permission) {
return true
}
if otherPermission == PermissionOwner && GteOwnerPermission(permission) {
return true
}
return false
}
func GetPermissionWeight(permission string) int {
if permission == PermissionViewer {
return 1
}
if permission == PermissionEditor {
return 2
}
if permission == PermissionOwner {
return 3
}
return 0
}

View File

@@ -0,0 +1,49 @@
package model
const (
SnapshotStatusNew = "new"
SnapshotStatusProcessing = "processing"
SnapshotStatusReady = "ready"
SnapshotStatusError = "error"
)
type Snapshot interface {
GetID() string
GetVersion() int64
GetOriginal() *S3Object
GetPreview() *S3Object
GetText() *S3Object
GetThumbnail() *Thumbnail
HasOriginal() bool
HasPreview() bool
HasText() bool
HasThumbnail() bool
GetStatus() string
GetCreateTime() string
GetUpdateTime() *string
SetID(string)
SetVersion(int64)
SetOriginal(*S3Object)
SetPreview(*S3Object)
SetText(*S3Object)
SetThumbnail(*Thumbnail)
SetStatus(string)
}
type S3Object struct {
Bucket string `json:"bucket"`
Key string `json:"key"`
Size int64 `json:"size"`
Image *ImageProps `json:"image,omitempty"`
}
type ImageProps struct {
Width int `json:"width"`
Height int `json:"height"`
}
type Thumbnail struct {
Base64 string `json:"base64"`
Width int `json:"width"`
Height int `json:"height"`
}

View File

@@ -0,0 +1,12 @@
package model
type User interface {
GetID() string
GetFullName() string
GetUsername() string
GetEmail() string
GetPicture() *string
GetIsEmailConfirmed() bool
GetCreateTime() string
GetUpdateTime() *string
}

View File

@@ -0,0 +1,16 @@
package model
type Workspace interface {
GetID() string
GetName() string
GetStorageCapacity() int64
GetRootID() string
GetOrganizationID() string
GetUserPermissions() []CoreUserPermission
GetGroupPermissions() []CoreGroupPermission
GetBucket() string
GetCreateTime() string
GetUpdateTime() *string
SetName(string)
SetUpdateTime(*string)
}

View File

@@ -0,0 +1,572 @@
package repo
import (
"errors"
"time"
"voltaserve/errorpkg"
"voltaserve/helper"
"voltaserve/infra"
"voltaserve/model"
"gorm.io/gorm"
)
type FileInsertOptions struct {
Name string
WorkspaceID string
ParentID *string
Type string
}
type FileRepo interface {
Insert(opts FileInsertOptions) (model.File, error)
Find(id string) (model.File, error)
FindChildren(id string) ([]model.File, error)
FindPath(id string) ([]model.File, error)
FindTree(id string) ([]model.File, error)
GetIDsByWorkspace(workspaceID string) ([]string, error)
AssignSnapshots(cloneID string, originalID string) error
MoveSourceIntoTarget(targetID string, sourceID string) error
Save(file model.File) error
BulkInsert(values []model.File, chunkSize int) error
BulkInsertPermissions(values []*UserPermission, chunkSize int) error
Delete(id string) error
GetChildrenIDs(id string) ([]string, error)
GetItemCount(id string) (int64, error)
IsGrandChildOf(id string, ancestorID string) (bool, error)
GetSize(id string) (int64, error)
GrantUserPermission(id string, userID string, permission string) error
RevokeUserPermission(id string, userID string) error
GrantGroupPermission(id string, groupID string, permission string) error
RevokeGroupPermission(id string, groupID string) error
}
func NewFileRepo() FileRepo {
return newFileRepo()
}
func NewFile() model.File {
return &fileEntity{}
}
type fileEntity struct {
ID string `json:"id" gorm:"column:id"`
WorkspaceID string `json:"workspaceId" gorm:"column:workspace_id"`
Name string `json:"name" gorm:"column:name"`
Type string `json:"type" gorm:"column:type"`
ParentID *string `json:"parentId,omitempty" gorm:"column:parent_id"`
Snapshots []*snapshotEntity `json:"snapshots,omitempty" gorm:"-"`
UserPermissions []*userPermissionValue `json:"userPermissions" gorm:"-"`
GroupPermissions []*groupPermissionValue `json:"groupPermissions" gorm:"-"`
Text *string `json:"text,omitempty" gorm:"-"`
CreateTime string `json:"createTime" gorm:"column:create_time"`
UpdateTime *string `json:"updateTime,omitempty" gorm:"column:update_time"`
}
func (*fileEntity) TableName() string {
return "file"
}
func (f *fileEntity) BeforeCreate(*gorm.DB) (err error) {
f.CreateTime = time.Now().UTC().Format(time.RFC3339)
return nil
}
func (f *fileEntity) BeforeSave(*gorm.DB) (err error) {
timeNow := time.Now().UTC().Format(time.RFC3339)
f.UpdateTime = &timeNow
return nil
}
func (f *fileEntity) GetID() string {
return f.ID
}
func (f *fileEntity) GetWorkspaceID() string {
return f.WorkspaceID
}
func (f *fileEntity) GetName() string {
return f.Name
}
func (f *fileEntity) GetType() string {
return f.Type
}
func (f *fileEntity) GetParentID() *string {
return f.ParentID
}
func (f *fileEntity) GetSnapshots() []model.Snapshot {
var res []model.Snapshot
for _, s := range f.Snapshots {
res = append(res, s)
}
return res
}
func (f *fileEntity) GetUserPermissions() []model.CoreUserPermission {
var res []model.CoreUserPermission
for _, p := range f.UserPermissions {
res = append(res, p)
}
return res
}
func (f *fileEntity) GetGroupPermissions() []model.CoreGroupPermission {
var res []model.CoreGroupPermission
for _, p := range f.GroupPermissions {
res = append(res, p)
}
return res
}
func (f *fileEntity) GetText() *string {
return f.Text
}
func (f *fileEntity) GetCreateTime() string {
return f.CreateTime
}
func (f *fileEntity) GetUpdateTime() *string {
return f.UpdateTime
}
func (f *fileEntity) SetID(id string) {
f.ID = id
}
func (f *fileEntity) SetParentID(parentID *string) {
f.ParentID = parentID
}
func (f *fileEntity) SetWorkspaceID(workspaceID string) {
f.WorkspaceID = workspaceID
}
func (f *fileEntity) SetType(fileType string) {
f.Type = fileType
}
func (f *fileEntity) SetName(name string) {
f.Name = name
}
func (f *fileEntity) SetText(text *string) {
f.Text = text
}
func (f *fileEntity) SetCreateTime(createTime string) {
f.CreateTime = createTime
}
func (f *fileEntity) SetUpdateTime(updateTime *string) {
f.UpdateTime = updateTime
}
type fileRepo struct {
db *gorm.DB
snapshotRepo *snapshotRepo
permissionRepo *permissionRepo
}
func newFileRepo() *fileRepo {
return &fileRepo{
db: infra.GetDb(),
snapshotRepo: newSnapshotRepo(),
permissionRepo: newPermissionRepo(),
}
}
func (repo *fileRepo) Insert(opts FileInsertOptions) (model.File, error) {
id := helper.NewID()
file := fileEntity{
ID: id,
WorkspaceID: opts.WorkspaceID,
Name: opts.Name,
Type: opts.Type,
ParentID: opts.ParentID,
}
if db := repo.db.Save(&file); db.Error != nil {
return nil, db.Error
}
res, err := repo.find(id)
if err != nil {
return nil, err
}
if err := repo.populateModelFields([]*fileEntity{res}); err != nil {
return nil, err
}
return res, nil
}
func (repo *fileRepo) Find(id string) (model.File, error) {
file, err := repo.find(id)
if err != nil {
return nil, err
}
if err := repo.populateModelFields([]*fileEntity{file}); err != nil {
return nil, err
}
return file, nil
}
func (repo *fileRepo) find(id string) (*fileEntity, error) {
var res = fileEntity{}
db := repo.db.Raw("SELECT * FROM file WHERE id = ?", id).Scan(&res)
if db.Error != nil {
if errors.Is(db.Error, gorm.ErrRecordNotFound) {
return nil, errorpkg.NewFileNotFoundError(db.Error)
} else {
return nil, errorpkg.NewInternalServerError(db.Error)
}
}
if len(res.ID) == 0 {
return nil, errorpkg.NewFileNotFoundError(db.Error)
}
return &res, nil
}
func (repo *fileRepo) FindChildren(id string) ([]model.File, error) {
var entities []*fileEntity
db := repo.db.Raw("SELECT * FROM file WHERE parent_id = ? ORDER BY create_time ASC", id).Scan(&entities)
if db.Error != nil {
return nil, db.Error
}
if err := repo.populateModelFields(entities); err != nil {
return nil, err
}
var res []model.File
for _, f := range entities {
res = append(res, f)
}
return res, nil
}
func (repo *fileRepo) FindPath(id string) ([]model.File, error) {
var entities []*fileEntity
if db := repo.db.
Raw("WITH RECURSIVE rec (id, name, type, parent_id, workspace_id, create_time, update_time) AS "+
"(SELECT f.id, f.name, f.type, f.parent_id, f.workspace_id, f.create_time, f.update_time FROM file f WHERE f.id = ? "+
"UNION SELECT f.id, f.name, f.type, f.parent_id, f.workspace_id, f.create_time, f.update_time FROM rec, file f WHERE f.id = rec.parent_id) "+
"SELECT * FROM rec", id).
Scan(&entities); db.Error != nil {
return nil, db.Error
}
if err := repo.populateModelFields(entities); err != nil {
return nil, err
}
var res []model.File
for _, f := range entities {
res = append(res, f)
}
return res, nil
}
func (repo *fileRepo) FindTree(id string) ([]model.File, error) {
var entities []*fileEntity
db := repo.db.
Raw("WITH RECURSIVE rec (id, name, type, parent_id, workspace_id, create_time, update_time) AS "+
"(SELECT f.id, f.name, f.type, f.parent_id, f.workspace_id, f.create_time, f.update_time FROM file f WHERE f.id = ? "+
"UNION SELECT f.id, f.name, f.type, f.parent_id, f.workspace_id, f.create_time, f.update_time FROM rec, file f WHERE f.parent_id = rec.id) "+
"SELECT rec.* FROM rec ORDER BY create_time ASC", id).
Scan(&entities)
if db.Error != nil {
return nil, db.Error
}
if err := repo.populateModelFields(entities); err != nil {
return nil, err
}
var res []model.File
for _, f := range entities {
res = append(res, f)
}
return res, nil
}
func (repo *fileRepo) GetIDsByWorkspace(workspaceID string) ([]string, error) {
type IDResult struct {
Result string
}
var ids []IDResult
db := repo.db.Raw("SELECT id result FROM file WHERE workspace_id = ? ORDER BY create_time ASC", workspaceID).Scan(&ids)
if db.Error != nil {
return nil, db.Error
}
res := []string{}
for _, id := range ids {
res = append(res, id.Result)
}
return res, nil
}
func (repo *fileRepo) AssignSnapshots(cloneID string, originalID string) error {
if db := repo.db.Exec("INSERT INTO snapshot_file (snapshot_id, file_id) SELECT s.id, ? "+
"FROM snapshot s LEFT JOIN snapshot_file map ON s.id = map.snapshot_id "+
"WHERE map.file_id = ? ORDER BY s.version DESC LIMIT 1", cloneID, originalID); db.Error != nil {
return db.Error
}
return nil
}
func (repo *fileRepo) MoveSourceIntoTarget(targetID string, sourceID string) error {
if db := repo.db.Exec("UPDATE file SET parent_id = ? WHERE id = ?", targetID, sourceID); db.Error != nil {
return db.Error
}
return nil
}
func (repo *fileRepo) Save(file model.File) error {
if db := repo.db.Save(file); db.Error != nil {
return db.Error
}
return nil
}
func (repo *fileRepo) BulkInsert(values []model.File, chunkSize int) error {
var entities []*fileEntity
for _, f := range values {
entities = append(entities, f.(*fileEntity))
}
if db := repo.db.CreateInBatches(entities, chunkSize); db.Error != nil {
return db.Error
}
return nil
}
func (repo *fileRepo) BulkInsertPermissions(values []*UserPermission, chunkSize int) error {
if db := repo.db.CreateInBatches(values, chunkSize); db.Error != nil {
return db.Error
}
return nil
}
func (repo *fileRepo) Delete(id string) error {
db := repo.db.Exec("DELETE FROM file WHERE id = ?", id)
if db.Error != nil {
return db.Error
}
db = repo.db.Exec("DELETE FROM userpermission WHERE resource_id = ?", id)
if db.Error != nil {
return db.Error
}
db = repo.db.Exec("DELETE FROM grouppermission WHERE resource_id = ?", id)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *fileRepo) GetChildrenIDs(id string) ([]string, error) {
type Result struct {
Result string
}
var results []Result
db := repo.db.Raw("SELECT id result FROM file WHERE parent_id = ? ORDER BY create_time ASC", id).Scan(&results)
if db.Error != nil {
return []string{}, db.Error
}
res := []string{}
for _, v := range results {
res = append(res, v.Result)
}
return res, nil
}
func (repo *fileRepo) GetItemCount(id string) (int64, error) {
type Result struct {
Result int64
}
var res Result
db := repo.db.
Raw("WITH RECURSIVE rec (id, parent_id) AS "+
"(SELECT f.id, f.parent_id FROM file f WHERE f.id = ? "+
"UNION SELECT f.id, f.parent_id FROM rec, file f WHERE f.parent_id = rec.id) "+
"SELECT count(rec.id) as result FROM rec", id).
Scan(&res)
if db.Error != nil {
return 0, db.Error
}
return res.Result - 1, nil
}
func (repo *fileRepo) IsGrandChildOf(id string, ancestorID string) (bool, error) {
type Result struct {
Result bool
}
var res Result
if db := repo.db.
Raw("WITH RECURSIVE rec (id, parent_id) AS "+
"(SELECT f.id, f.parent_id FROM file f WHERE f.id = ? "+
"UNION SELECT f.id, f.parent_id FROM rec, file f WHERE f.parent_id = rec.id) "+
"SELECT count(rec.id) > 0 as result FROM rec WHERE rec.id = ?", ancestorID, id).
Scan(&res); db.Error != nil {
return false, db.Error
}
return res.Result, nil
}
func (repo *fileRepo) GetSize(id string) (int64, error) {
type Result struct {
Result int64
}
var res Result
db := repo.db.
Raw("WITH RECURSIVE rec (id, parent_id) AS "+
"(SELECT f.id, f.parent_id FROM file f WHERE f.id = ? "+
"UNION SELECT f.id, f.parent_id FROM rec, file f WHERE f.parent_id = rec.id) "+
"SELECT coalesce(sum((s.original->>'size')::int), 0) as result FROM snapshot s, rec "+
"LEFT JOIN snapshot_file map ON rec.id = map.file_id WHERE map.snapshot_id = s.id", id).
Scan(&res)
if db.Error != nil {
return res.Result, db.Error
}
return res.Result, nil
}
func (repo *fileRepo) GrantUserPermission(id string, userID string, permission string) error {
/* Grant permission to workspace */
db := repo.db.Exec("INSERT INTO userpermission (id, user_id, resource_id, permission) "+
"(SELECT ?, ?, w.id, 'viewer' FROM file f "+
"INNER JOIN workspace w ON w.id = f.workspace_id AND f.id = ?) "+
"ON CONFLICT DO NOTHING",
helper.NewID(), userID, id)
if db.Error != nil {
return db.Error
}
/* Grant 'viewer' permission to path files */
path, err := repo.FindPath(id)
if err != nil {
return err
}
for _, f := range path {
db := repo.db.Exec("INSERT INTO userpermission (id, user_id, resource_id, permission) "+
"VALUES (?, ?, ?, 'viewer') ON CONFLICT DO NOTHING",
helper.NewID(), userID, f.GetID())
if db.Error != nil {
return db.Error
}
}
/* Grant the requested permission to tree files */
tree, err := repo.FindTree(id)
if err != nil {
return err
}
for _, f := range tree {
db := repo.db.Exec("INSERT INTO userpermission (id, user_id, resource_id, permission) "+
"VALUES (?, ?, ?, ?) ON CONFLICT (user_id, resource_id) DO UPDATE SET permission = ?",
helper.NewID(), userID, f.GetID(), permission, permission)
if db.Error != nil {
return db.Error
}
}
return nil
}
func (repo *fileRepo) RevokeUserPermission(id string, userID string) error {
tree, err := repo.FindTree(id)
if err != nil {
return err
}
for _, f := range tree {
db := repo.db.Exec("DELETE FROM userpermission WHERE user_id = ? AND resource_id = ?", userID, f.GetID())
if db.Error != nil {
return db.Error
}
}
return nil
}
func (repo *fileRepo) GrantGroupPermission(id string, groupID string, permission string) error {
/* Grant permission to workspace */
db := repo.db.Exec("INSERT INTO grouppermission (id, group_id, resource_id, permission) "+
"(SELECT ?, ?, w.id, 'viewer' FROM file f "+
"INNER JOIN workspace w ON w.id = f.workspace_id AND f.id = ?) "+
"ON CONFLICT DO NOTHING",
helper.NewID(), groupID, id)
if db.Error != nil {
return db.Error
}
/* Grant 'viewer' permission to path files */
path, err := repo.FindPath(id)
if err != nil {
return err
}
for _, f := range path {
db := repo.db.Exec("INSERT INTO grouppermission (id, group_id, resource_id, permission) "+
"VALUES (?, ?, ?, 'viewer') ON CONFLICT DO NOTHING",
helper.NewID(), groupID, f.GetID())
if db.Error != nil {
return db.Error
}
}
/* Grant the requested permission to tree files */
tree, err := repo.FindTree(id)
if err != nil {
return err
}
for _, f := range tree {
db := repo.db.Exec("INSERT INTO grouppermission (id, group_id, resource_id, permission) "+
"VALUES (?, ?, ?, ?) ON CONFLICT (group_id, resource_id) DO UPDATE SET permission = ?",
helper.NewID(), groupID, f.GetID(), permission, permission)
if db.Error != nil {
return db.Error
}
}
return nil
}
func (repo *fileRepo) RevokeGroupPermission(id string, groupID string) error {
tree, err := repo.FindTree(id)
if err != nil {
return err
}
for _, f := range tree {
db := repo.db.Exec("DELETE FROM grouppermission WHERE group_id = ? AND resource_id = ?", groupID, f.GetID())
if db.Error != nil {
return db.Error
}
}
return nil
}
func (repo *fileRepo) populateModelFields(entities []*fileEntity) error {
for _, f := range entities {
f.UserPermissions = make([]*userPermissionValue, 0)
userPermissions, err := repo.permissionRepo.GetUserPermissions(f.ID)
if err != nil {
return err
}
for _, p := range userPermissions {
f.UserPermissions = append(f.UserPermissions, &userPermissionValue{
UserID: p.UserID,
Value: p.Permission,
})
}
f.GroupPermissions = make([]*groupPermissionValue, 0)
groupPermissions, err := repo.permissionRepo.GetGroupPermissions(f.ID)
if err != nil {
return err
}
for _, p := range groupPermissions {
f.GroupPermissions = append(f.GroupPermissions, &groupPermissionValue{
GroupID: p.GroupID,
Value: p.Permission,
})
}
snapshots, err := repo.snapshotRepo.findAllForFile(f.ID)
if err != nil {
return nil
}
f.Snapshots = snapshots
}
return nil
}

View File

@@ -0,0 +1,344 @@
package repo
import (
"errors"
"time"
"voltaserve/errorpkg"
"voltaserve/helper"
"voltaserve/infra"
"voltaserve/model"
"gorm.io/gorm"
)
type GroupInsertOptions struct {
ID string
Name string
OrganizationID string
OwnerID string
}
type GroupRepo interface {
Insert(opts GroupInsertOptions) (model.Group, error)
Find(id string) (model.Group, error)
GetIDsForFile(fileID string) ([]string, error)
GetIDsForUser(userID string) ([]string, error)
GetIDsForOrganization(id string) ([]string, error)
Save(group model.Group) error
Delete(id string) error
AddUser(id string, userID string) error
RemoveMember(id string, userID string) error
GetIDs() ([]string, error)
GetMembers(id string) ([]model.User, error)
GrantUserPermission(id string, userID string, permission string) error
RevokeUserPermission(id string, userID string) error
}
func NewGroupRepo() GroupRepo {
return newGroupRepo()
}
func NewGroup() model.Group {
return &groupEntity{}
}
type groupEntity struct {
ID string `json:"id" gorm:"column:id"`
Name string `json:"name" gorm:"column:name"`
OrganizationID string `json:"organizationId" gorm:"column:organization_id"`
UserPermissions []*userPermissionValue `json:"userPermissions" gorm:"-"`
GroupPermissions []*groupPermissionValue `json:"groupPermissions" gorm:"-"`
Members []string `json:"members" gorm:"-"`
CreateTime string `json:"createTime" gorm:"column:create_time"`
UpdateTime *string `json:"updateTime" gorm:"column:update_time"`
}
func (*groupEntity) TableName() string {
return "group"
}
func (g *groupEntity) BeforeCreate(*gorm.DB) (err error) {
g.CreateTime = time.Now().UTC().Format(time.RFC3339)
return nil
}
func (g *groupEntity) BeforeSave(*gorm.DB) (err error) {
timeNow := time.Now().UTC().Format(time.RFC3339)
g.UpdateTime = &timeNow
return nil
}
func (g *groupEntity) GetID() string {
return g.ID
}
func (g *groupEntity) GetName() string {
return g.Name
}
func (g *groupEntity) GetOrganizationID() string {
return g.OrganizationID
}
func (g *groupEntity) GetUserPermissions() []model.CoreUserPermission {
var res []model.CoreUserPermission
for _, p := range g.UserPermissions {
res = append(res, p)
}
return res
}
func (g *groupEntity) GetGroupPermissions() []model.CoreGroupPermission {
var res []model.CoreGroupPermission
for _, p := range g.GroupPermissions {
res = append(res, p)
}
return res
}
func (g *groupEntity) GetUsers() []string {
return g.Members
}
func (g *groupEntity) GetCreateTime() string {
return g.CreateTime
}
func (g *groupEntity) GetUpdateTime() *string {
return g.UpdateTime
}
func (g *groupEntity) SetName(name string) {
g.Name = name
}
func (g *groupEntity) SetUpdateTime(updateTime *string) {
g.UpdateTime = updateTime
}
type groupRepo struct {
db *gorm.DB
permissionRepo *permissionRepo
}
func newGroupRepo() *groupRepo {
return &groupRepo{
db: infra.GetDb(),
permissionRepo: newPermissionRepo(),
}
}
func (repo *groupRepo) Insert(opts GroupInsertOptions) (model.Group, error) {
group := groupEntity{
ID: opts.ID,
Name: opts.Name,
OrganizationID: opts.OrganizationID,
}
if db := repo.db.Save(&group); db.Error != nil {
return nil, db.Error
}
res, err := repo.Find(opts.ID)
if err != nil {
return nil, err
}
return res, nil
}
func (repo *groupRepo) find(id string) (*groupEntity, error) {
var res = groupEntity{}
db := repo.db.Where("id = ?", id).First(&res)
if db.Error != nil {
if errors.Is(db.Error, gorm.ErrRecordNotFound) {
return nil, errorpkg.NewGroupNotFoundError(db.Error)
} else {
return nil, errorpkg.NewInternalServerError(db.Error)
}
}
return &res, nil
}
func (repo *groupRepo) Find(id string) (model.Group, error) {
group, err := repo.find(id)
if err != nil {
return nil, err
}
if err := repo.populateModelFields([]*groupEntity{group}); err != nil {
return nil, err
}
return group, nil
}
func (repo *groupRepo) GetIDsForFile(fileID string) ([]string, error) {
type Result struct {
Result string
}
var results []Result
db := repo.db.
Raw(`SELECT DISTINCT g.id as result FROM "group" g INNER JOIN grouppermission p ON p.resource_id = ? WHERE p.group_id = g.id`, fileID).
Scan(&results)
if db.Error != nil {
return []string{}, db.Error
}
res := []string{}
for _, v := range results {
res = append(res, v.Result)
}
return res, nil
}
func (repo *groupRepo) GetIDsForUser(userID string) ([]string, error) {
type Result struct {
Result string
}
var results []Result
db := repo.db.Raw(`SELECT group_id from group_user WHERE user_id = ?`, userID).Scan(&results)
if db.Error != nil {
return []string{}, db.Error
}
res := []string{}
for _, v := range results {
res = append(res, v.Result)
}
return res, nil
}
func (repo *groupRepo) GetIDsForOrganization(id string) ([]string, error) {
type Result struct {
Result string
}
var results []Result
db := repo.db.Raw(`SELECT id as result from "group" WHERE organization_id = ?`, id).Scan(&results)
if db.Error != nil {
return []string{}, db.Error
}
res := []string{}
for _, v := range results {
res = append(res, v.Result)
}
return res, nil
}
func (repo *groupRepo) Save(group model.Group) error {
db := repo.db.Save(group)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *groupRepo) Delete(id string) error {
db := repo.db.Exec(`DELETE FROM "group" WHERE id = ?`, id)
if db.Error != nil {
return db.Error
}
db = repo.db.Exec("DELETE FROM userpermission WHERE resource_id = ?", id)
if db.Error != nil {
return db.Error
}
db = repo.db.Exec("DELETE FROM grouppermission WHERE resource_id = ?", id)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *groupRepo) AddUser(id string, userID string) error {
db := repo.db.Exec("INSERT INTO group_user (group_id, user_id) VALUES (?, ?)", id, userID)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *groupRepo) RemoveMember(id string, userID string) error {
db := repo.db.Exec("DELETE FROM group_user WHERE group_id = ? AND user_id = ?", id, userID)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *groupRepo) GetIDs() ([]string, error) {
type Result struct {
Result string
}
var results []Result
db := repo.db.Raw(`SELECT id result FROM "group" ORDER BY create_time DESC`).Scan(&results)
if db.Error != nil {
return []string{}, db.Error
}
res := []string{}
for _, v := range results {
res = append(res, v.Result)
}
return res, nil
}
func (repo *groupRepo) GetMembers(id string) ([]model.User, error) {
var entities []*userEntity
db := repo.db.
Raw(`SELECT DISTINCT u.* FROM "user" u INNER JOIN group_user gu ON u.id = gu.user_id WHERE gu.group_id = ?`, id).
Scan(&entities)
if db.Error != nil {
return nil, db.Error
}
var res []model.User
for _, u := range entities {
res = append(res, u)
}
return res, nil
}
func (repo *groupRepo) GrantUserPermission(id string, userID string, permission string) error {
db := repo.db.Exec(
"INSERT INTO userpermission (id, user_id, resource_id, permission) VALUES (?, ?, ?, ?) ON CONFLICT (user_id, resource_id) DO UPDATE SET permission = ?",
helper.NewID(), userID, id, permission, permission)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *groupRepo) RevokeUserPermission(id string, userID string) error {
db := repo.db.Exec("DELETE FROM userpermission WHERE user_id = ? AND resource_id = ?", userID, id)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *groupRepo) populateModelFields(groups []*groupEntity) error {
for _, g := range groups {
g.UserPermissions = make([]*userPermissionValue, 0)
userPermissions, err := repo.permissionRepo.GetUserPermissions(g.ID)
if err != nil {
return err
}
for _, p := range userPermissions {
g.UserPermissions = append(g.UserPermissions, &userPermissionValue{
UserID: p.UserID,
Value: p.Permission,
})
}
g.GroupPermissions = make([]*groupPermissionValue, 0)
groupPermissions, err := repo.permissionRepo.GetGroupPermissions(g.ID)
if err != nil {
return err
}
for _, p := range groupPermissions {
g.GroupPermissions = append(g.GroupPermissions, &groupPermissionValue{
GroupID: p.GroupID,
Value: p.Permission,
})
}
members, err := repo.GetMembers(g.ID)
if err != nil {
return nil
}
g.Members = make([]string, 0)
for _, u := range members {
g.Members = append(g.Members, u.GetID())
}
}
return nil
}

View File

@@ -0,0 +1,185 @@
package repo
import (
"errors"
"time"
"voltaserve/errorpkg"
"voltaserve/helper"
"voltaserve/infra"
"voltaserve/model"
"gorm.io/gorm"
)
type InvitationInsertOptions struct {
UserID string
OrganizationID string
Emails []string
}
type InvitationRepo interface {
Insert(opts InvitationInsertOptions) ([]model.Invitation, error)
Find(id string) (model.Invitation, error)
GetIncoming(email string) ([]model.Invitation, error)
GetOutgoing(orgID string, userID string) ([]model.Invitation, error)
Save(org model.Invitation) error
Delete(id string) error
}
func NewInvitationRepo() InvitationRepo {
return newInvitationRepo()
}
type invitationEntity struct {
ID string `json:"id" gorm:"column:id"`
OrganizationID string `json:"organizationId" gorm:"column:organization_id"`
OwnerID string `json:"ownerId" gorm:"column:owner_id"`
Email string `json:"email" gorm:"column:email"`
Status string `json:"status" gorm:"column:status"`
CreateTime string `json:"createTime" gorm:"column:create_time"`
UpdateTime *string `json:"updateTime" gorm:"column:update_time"`
}
func (*invitationEntity) TableName() string {
return "invitation"
}
func (i *invitationEntity) BeforeCreate(*gorm.DB) (err error) {
i.CreateTime = time.Now().UTC().Format(time.RFC3339)
return nil
}
func (i *invitationEntity) BeforeSave(*gorm.DB) (err error) {
timeNow := time.Now().UTC().Format(time.RFC3339)
i.UpdateTime = &timeNow
return nil
}
func (i *invitationEntity) GetID() string {
return i.ID
}
func (i *invitationEntity) GetOrganizationID() string {
return i.OrganizationID
}
func (i *invitationEntity) GetOwnerID() string {
return i.OwnerID
}
func (i *invitationEntity) GetEmail() string {
return i.Email
}
func (i *invitationEntity) GetStatus() string {
return i.Status
}
func (i *invitationEntity) GetCreateTime() string {
return i.CreateTime
}
func (i *invitationEntity) GetUpdateTime() *string {
return i.UpdateTime
}
func (i *invitationEntity) SetStatus(status string) {
i.Status = status
}
func (i *invitationEntity) SetUpdateTime(updateTime *string) {
i.UpdateTime = updateTime
}
type invitationRepo struct {
db *gorm.DB
userRepo *userRepo
}
func newInvitationRepo() *invitationRepo {
return &invitationRepo{
db: infra.GetDb(),
userRepo: newUserRepo(),
}
}
func (repo *invitationRepo) Insert(opts InvitationInsertOptions) ([]model.Invitation, error) {
var res []model.Invitation
for _, e := range opts.Emails {
invitation := invitationEntity{
ID: helper.NewID(),
OrganizationID: opts.OrganizationID,
OwnerID: opts.UserID,
Email: e,
Status: model.InvitationStatusPending,
}
if db := repo.db.Save(&invitation); db.Error != nil {
return nil, db.Error
}
i, err := repo.Find(invitation.ID)
if err != nil {
return nil, err
}
res = append(res, i)
}
return res, nil
}
func (repo *invitationRepo) Find(id string) (model.Invitation, error) {
var invitation = invitationEntity{}
db := repo.db.Where("id = ?", id).First(&invitation)
if db.Error != nil {
if errors.Is(db.Error, gorm.ErrRecordNotFound) {
return nil, errorpkg.NewInvitationNotFoundError(db.Error)
} else {
return nil, errorpkg.NewInternalServerError(db.Error)
}
}
return &invitation, nil
}
func (repo *invitationRepo) GetIncoming(email string) ([]model.Invitation, error) {
var invitations []*invitationEntity
db := repo.db.
Raw("SELECT * FROM invitation WHERE email = ? and status = 'pending' ORDER BY create_time DESC", email).
Scan(&invitations)
if db.Error != nil {
return nil, db.Error
}
var res []model.Invitation
for _, inv := range invitations {
res = append(res, inv)
}
return res, nil
}
func (repo *invitationRepo) GetOutgoing(orgID string, userID string) ([]model.Invitation, error) {
var invitations []*invitationEntity
db := repo.db.
Raw("SELECT * FROM invitation WHERE organization_id = ? and owner_id = ? ORDER BY create_time DESC", orgID, userID).
Scan(&invitations)
if db.Error != nil {
return nil, db.Error
}
var res []model.Invitation
for _, inv := range invitations {
res = append(res, inv)
}
return res, nil
}
func (repo *invitationRepo) Save(org model.Invitation) error {
db := repo.db.Save(org)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *invitationRepo) Delete(id string) error {
db := repo.db.Exec("DELETE FROM invitation WHERE id = ?", id)
if db.Error != nil {
return db.Error
}
return nil
}

View File

@@ -0,0 +1,319 @@
package repo
import (
"errors"
"time"
"voltaserve/errorpkg"
"voltaserve/helper"
"voltaserve/infra"
"voltaserve/model"
"gorm.io/gorm"
)
type OrganizationInsertOptions struct {
ID string
Name string
}
type OrganizationRepo interface {
Insert(opts OrganizationInsertOptions) (model.Organization, error)
Find(id string) (model.Organization, error)
Save(org model.Organization) error
Delete(id string) error
GetIDs() ([]string, error)
AddUser(id string, userID string) error
RemoveMember(id string, userID string) error
GetMembers(id string) ([]model.User, error)
GetGroups(id string) ([]model.Group, error)
GetOwnerCount(id string) (int64, error)
GrantUserPermission(id string, userID string, permission string) error
RevokeUserPermission(id string, userID string) error
}
func NewOrganizationRepo() OrganizationRepo {
return newOrganizationRepo()
}
func NewOrganization() model.Organization {
return &organizationEntity{}
}
type organizationEntity struct {
ID string `json:"id" gorm:"column:id"`
Name string `json:"name" gorm:"column:name"`
UserPermissions []*userPermissionValue `json:"userPermissions" gorm:"-"`
GroupPermissions []*groupPermissionValue `json:"groupPermissions" gorm:"-"`
Members []string `json:"members" gorm:"-"`
CreateTime string `json:"createTime" gorm:"column:create_time"`
UpdateTime *string `json:"updateTime,omitempty" gorm:"column:update_time"`
}
func (*organizationEntity) TableName() string {
return "organization"
}
func (o *organizationEntity) BeforeCreate(*gorm.DB) (err error) {
o.CreateTime = time.Now().UTC().Format(time.RFC3339)
return nil
}
func (o *organizationEntity) BeforeSave(*gorm.DB) (err error) {
timeNow := time.Now().UTC().Format(time.RFC3339)
o.UpdateTime = &timeNow
return nil
}
func (o *organizationEntity) GetID() string {
return o.ID
}
func (o *organizationEntity) GetName() string {
return o.Name
}
func (o *organizationEntity) GetUserPermissions() []model.CoreUserPermission {
var res []model.CoreUserPermission
for _, p := range o.UserPermissions {
res = append(res, p)
}
return res
}
func (o *organizationEntity) GetGroupPermissions() []model.CoreGroupPermission {
var res []model.CoreGroupPermission
for _, p := range o.GroupPermissions {
res = append(res, p)
}
return res
}
func (o *organizationEntity) GetUsers() []string {
return o.Members
}
func (o *organizationEntity) GetCreateTime() string {
return o.CreateTime
}
func (o *organizationEntity) GetUpdateTime() *string {
return o.UpdateTime
}
func (o *organizationEntity) SetName(name string) {
o.Name = name
}
func (o *organizationEntity) SetUpdateTime(updateTime *string) {
o.UpdateTime = updateTime
}
type organizationRepo struct {
db *gorm.DB
groupRepo *groupRepo
permissionRepo *permissionRepo
}
func newOrganizationRepo() *organizationRepo {
return &organizationRepo{
db: infra.GetDb(),
groupRepo: newGroupRepo(),
permissionRepo: newPermissionRepo(),
}
}
func (repo *organizationRepo) Insert(opts OrganizationInsertOptions) (model.Organization, error) {
org := organizationEntity{
ID: opts.ID,
Name: opts.Name,
}
if db := repo.db.Save(&org); db.Error != nil {
return nil, db.Error
}
res, err := repo.Find(opts.ID)
if err != nil {
return nil, err
}
return res, nil
}
func (repo *organizationRepo) find(id string) (*organizationEntity, error) {
var res = organizationEntity{}
db := repo.db.Where("id = ?", id).First(&res)
if db.Error != nil {
if errors.Is(db.Error, gorm.ErrRecordNotFound) {
return nil, errorpkg.NewOrganizationNotFoundError(db.Error)
} else {
return nil, errorpkg.NewInternalServerError(db.Error)
}
}
return &res, nil
}
func (repo *organizationRepo) Find(id string) (model.Organization, error) {
org, err := repo.find(id)
if err != nil {
return nil, err
}
if err := repo.populateModelFields([]*organizationEntity{org}); err != nil {
return nil, err
}
return org, nil
}
func (repo *organizationRepo) Save(org model.Organization) error {
db := repo.db.Save(org)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *organizationRepo) Delete(id string) error {
db := repo.db.Exec("DELETE FROM organization WHERE id = ?", id)
if db.Error != nil {
return db.Error
}
db = repo.db.Exec("DELETE FROM userpermission WHERE resource_id = ?", id)
if db.Error != nil {
return db.Error
}
db = repo.db.Exec("DELETE FROM grouppermission WHERE resource_id = ?", id)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *organizationRepo) GetIDs() ([]string, error) {
type Result struct {
Result string
}
var results []Result
db := repo.db.Raw("SELECT id result FROM organization ORDER BY create_time DESC").Scan(&results)
if db.Error != nil {
return []string{}, db.Error
}
res := []string{}
for _, v := range results {
res = append(res, v.Result)
}
return res, nil
}
func (repo *organizationRepo) AddUser(id string, userID string) error {
db := repo.db.Exec("INSERT INTO organization_user (organization_id, user_id) VALUES (?, ?)", id, userID)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *organizationRepo) RemoveMember(id string, userID string) error {
db := repo.db.Exec("DELETE FROM organization_user WHERE organization_id = ? AND user_id = ?", id, userID)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *organizationRepo) GetMembers(id string) ([]model.User, error) {
var entities []*userEntity
db := repo.db.
Raw(`SELECT DISTINCT u.* FROM "user" u INNER JOIN organization_user ou ON u.id = ou.user_id WHERE ou.organization_id = ? ORDER BY u.full_name`, id).
Scan(&entities)
if db.Error != nil {
return nil, db.Error
}
var res []model.User
for _, u := range entities {
res = append(res, u)
}
return res, nil
}
func (repo *organizationRepo) GetGroups(id string) ([]model.Group, error) {
var entities []*groupEntity
db := repo.db.
Raw(`SELECT * FROM "group" g WHERE g.organization_id = ? ORDER BY g.name`, id).
Scan(&entities)
if db.Error != nil {
return nil, db.Error
}
if err := repo.groupRepo.populateModelFields(entities); err != nil {
return nil, err
}
var res []model.Group
for _, g := range entities {
res = append(res, g)
}
return res, nil
}
func (repo *organizationRepo) GetOwnerCount(id string) (int64, error) {
type Result struct {
Result int64
}
var res Result
db := repo.db.
Raw("SELECT count(*) as result FROM userpermission WHERE resource_id = ? and permission = ?", id, model.PermissionOwner).
Scan(&res)
if db.Error != nil {
return 0, db.Error
}
return res.Result, nil
}
func (repo *organizationRepo) GrantUserPermission(id string, userID string, permission string) error {
db := repo.db.Exec(
"INSERT INTO userpermission (id, user_id, resource_id, permission) VALUES (?, ?, ?, ?) ON CONFLICT (user_id, resource_id) DO UPDATE SET permission = ?",
helper.NewID(), userID, id, permission, permission)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *organizationRepo) RevokeUserPermission(id string, userID string) error {
db := repo.db.Exec("DELETE FROM userpermission WHERE user_id = ? AND resource_id = ?", userID, id)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *organizationRepo) populateModelFields(organizations []*organizationEntity) error {
for _, o := range organizations {
o.UserPermissions = make([]*userPermissionValue, 0)
userPermissions, err := repo.permissionRepo.GetUserPermissions(o.ID)
if err != nil {
return err
}
for _, p := range userPermissions {
o.UserPermissions = append(o.UserPermissions, &userPermissionValue{
UserID: p.UserID,
Value: p.Permission,
})
}
o.GroupPermissions = make([]*groupPermissionValue, 0)
groupPermissions, err := repo.permissionRepo.GetGroupPermissions(o.ID)
if err != nil {
return err
}
for _, p := range groupPermissions {
o.GroupPermissions = append(o.GroupPermissions, &groupPermissionValue{
GroupID: p.GroupID,
Value: p.Permission,
})
}
members, err := repo.GetMembers(o.ID)
if err != nil {
return nil
}
o.Members = make([]string, 0)
for _, u := range members {
o.Members = append(o.Members, u.GetID())
}
}
return nil
}

View File

@@ -0,0 +1,102 @@
package repo
import (
"voltaserve/infra"
"gorm.io/gorm"
)
type UserPermission struct {
ID string `json:"id" gorm:"column:id"`
UserID string `json:"userId" gorm:"column:user_id"`
ResourceID string `json:"resourceId" gorm:"column:resource_id"`
Permission string `json:"permission" gorm:"column:permission"`
CreateTime string `json:"createTime" gorm:"column:create_time"`
}
type GroupPermission struct {
ID string `json:"id" gorm:"column:id"`
GroupID string `json:"groupId" gorm:"column:group_id"`
ResourceID string `json:"resourceId" gorm:"column:resource_id"`
Permission string `json:"permission" gorm:"column:permission"`
CreateTime string `json:"createTime" gorm:"column:create_time"`
}
type PermissionRepo interface {
GetUserPermissions(id string) ([]*UserPermission, error)
GetGroupPermissions(id string) ([]*GroupPermission, error)
}
func NewPermissionRepo() PermissionRepo {
return newPermissionRepo()
}
func (UserPermission) TableName() string {
return "userpermission"
}
func (GroupPermission) TableName() string {
return "grouppermission"
}
type userPermissionValue struct {
UserID string `json:"userId,omitempty"`
Value string `json:"value,omitempty"`
}
func (p userPermissionValue) GetUserID() string {
return p.UserID
}
func (p userPermissionValue) GetValue() string {
return p.Value
}
type groupPermissionValue struct {
GroupID string `json:"groupId,omitempty"`
Value string `json:"value,omitempty"`
}
func (p groupPermissionValue) GetGroupID() string {
return p.GroupID
}
func (p groupPermissionValue) GetValue() string {
return p.Value
}
type permissionRepo struct {
db *gorm.DB
}
func newPermissionRepo() *permissionRepo {
return &permissionRepo{
db: infra.GetDb(),
}
}
func (repo *permissionRepo) GetUserPermissions(id string) ([]*UserPermission, error) {
var res []*UserPermission
if db := repo.db.
Raw("SELECT * FROM userpermission WHERE resource_id = ?", id).
Scan(&res); db.Error != nil {
return nil, db.Error
}
if len(res) > 0 {
return res, nil
} else {
return []*UserPermission{}, nil
}
}
func (repo *permissionRepo) GetGroupPermissions(id string) ([]*GroupPermission, error) {
var res []*GroupPermission
if db := repo.db.
Raw("SELECT * FROM grouppermission WHERE resource_id = ?", id).
Scan(&res); db.Error != nil {
return nil, db.Error
}
if len(res) > 0 {
return res, nil
} else {
return []*GroupPermission{}, nil
}
}

View File

@@ -0,0 +1,345 @@
package repo
import (
"encoding/json"
"errors"
"log"
"time"
"voltaserve/errorpkg"
"voltaserve/infra"
"voltaserve/model"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type SnapshotUpdateOptions struct {
Original *model.S3Object
Preview *model.S3Object
Text *model.S3Object
Thumbnail *model.Thumbnail
Status string
}
type SnapshotRepo interface {
Find(id string) (model.Snapshot, error)
Save(snapshot model.Snapshot) error
Update(id string, opts SnapshotUpdateOptions) error
MapWithFile(id string, fileID string) error
DeleteMappingsForFile(fileID string) error
FindAllDangling() ([]model.Snapshot, error)
DeleteAllDangling() error
GetLatestVersionForFile(fileID string) (int64, error)
}
func NewSnapshotRepo() SnapshotRepo {
return newSnapshotRepo()
}
func NewSnapshot() model.Snapshot {
return &snapshotEntity{}
}
type snapshotEntity struct {
ID string `json:"id" gorm:"column:id;size:36"`
Version int64 `json:"version" gorm:"column:version"`
Original datatypes.JSON `json:"original,omitempty" gorm:"column:original"`
Preview datatypes.JSON `json:"preview,omitempty" gorm:"column:preview"`
Text datatypes.JSON `json:"text,omitempty" gorm:"column:text"`
Thumbnail datatypes.JSON `json:"thumbnail,omitempty" gorm:"column:thumbnail"`
Status string `json:"status,omitempty" gorm:"column,status"`
CreateTime string `json:"createTime" gorm:"column:create_time"`
UpdateTime *string `json:"updateTime,omitempty" gorm:"column:update_time"`
}
func (*snapshotEntity) TableName() string {
return "snapshot"
}
func (s *snapshotEntity) BeforeCreate(*gorm.DB) (err error) {
s.CreateTime = time.Now().UTC().Format(time.RFC3339)
return nil
}
func (s *snapshotEntity) BeforeSave(*gorm.DB) (err error) {
timeNow := time.Now().UTC().Format(time.RFC3339)
s.UpdateTime = &timeNow
return nil
}
func (s *snapshotEntity) GetID() string {
return s.ID
}
func (s *snapshotEntity) GetVersion() int64 {
return s.Version
}
func (s *snapshotEntity) GetOriginal() *model.S3Object {
if s.Original.String() == "" {
return nil
}
var res = model.S3Object{}
if err := json.Unmarshal([]byte(s.Original.String()), &res); err != nil {
log.Fatal(err)
return nil
}
return &res
}
func (s *snapshotEntity) GetPreview() *model.S3Object {
if s.Preview.String() == "" {
return nil
}
var res = model.S3Object{}
if err := json.Unmarshal([]byte(s.Preview.String()), &res); err != nil {
log.Fatal(err)
return nil
}
return &res
}
func (s *snapshotEntity) GetText() *model.S3Object {
if s.Text.String() == "" {
return nil
}
var res = model.S3Object{}
if err := json.Unmarshal([]byte(s.Text.String()), &res); err != nil {
log.Fatal(err)
return nil
}
return &res
}
func (s *snapshotEntity) GetThumbnail() *model.Thumbnail {
if s.Thumbnail.String() == "" {
return nil
}
var res = model.Thumbnail{}
if err := json.Unmarshal([]byte(s.Thumbnail.String()), &res); err != nil {
log.Fatal(err)
return nil
}
return &res
}
func (s *snapshotEntity) GetStatus() string {
return s.Status
}
func (s *snapshotEntity) SetID(id string) {
s.ID = id
}
func (s *snapshotEntity) SetVersion(version int64) {
s.Version = version
}
func (s *snapshotEntity) SetOriginal(m *model.S3Object) {
if m == nil {
s.Original = nil
} else {
b, err := json.Marshal(m)
if err != nil {
log.Fatal(err)
return
}
if err := s.Original.UnmarshalJSON(b); err != nil {
log.Fatal(err)
}
}
}
func (s *snapshotEntity) SetPreview(m *model.S3Object) {
if m == nil {
s.Preview = nil
} else {
b, err := json.Marshal(m)
if err != nil {
log.Fatal(err)
return
}
if err := s.Preview.UnmarshalJSON(b); err != nil {
log.Fatal(err)
}
}
}
func (s *snapshotEntity) SetText(m *model.S3Object) {
if m == nil {
s.Text = nil
} else {
b, err := json.Marshal(m)
if err != nil {
log.Fatal(err)
return
}
if err := s.Text.UnmarshalJSON(b); err != nil {
log.Fatal(err)
}
}
}
func (s *snapshotEntity) SetThumbnail(m *model.Thumbnail) {
if m == nil {
s.Thumbnail = nil
} else {
b, err := json.Marshal(m)
if err != nil {
log.Fatal(err)
return
}
if err := s.Thumbnail.UnmarshalJSON(b); err != nil {
log.Fatal(err)
}
}
}
func (s *snapshotEntity) SetStatus(status string) {
s.Status = status
}
func (s *snapshotEntity) HasOriginal() bool {
return s.Original != nil
}
func (s *snapshotEntity) HasPreview() bool {
return s.Preview != nil
}
func (s *snapshotEntity) HasText() bool {
return s.Text != nil
}
func (s *snapshotEntity) HasThumbnail() bool {
return s.Thumbnail != nil
}
func (s *snapshotEntity) GetCreateTime() string {
return s.CreateTime
}
func (s *snapshotEntity) GetUpdateTime() *string {
return s.UpdateTime
}
type snapshotRepo struct {
db *gorm.DB
}
func newSnapshotRepo() *snapshotRepo {
return &snapshotRepo{
db: infra.GetDb(),
}
}
func (repo *snapshotRepo) find(id string) (*snapshotEntity, error) {
var res snapshotEntity
if db := repo.db.Where("id = ?", id).First(&res); db.Error != nil {
if errors.Is(db.Error, gorm.ErrRecordNotFound) {
return nil, errorpkg.NewSnapshotNotFoundError(db.Error)
} else {
return nil, errorpkg.NewInternalServerError(db.Error)
}
}
return &res, nil
}
func (repo *snapshotRepo) Find(id string) (model.Snapshot, error) {
res, err := repo.find(id)
if err != nil {
return nil, err
}
return res, nil
}
func (repo *snapshotRepo) Save(snapshot model.Snapshot) error {
if db := repo.db.Save(snapshot); db.Error != nil {
return db.Error
}
return nil
}
func (repo *snapshotRepo) Update(id string, opts SnapshotUpdateOptions) error {
snapshot, err := repo.find(id)
if err != nil {
return err
}
if opts.Thumbnail != nil {
snapshot.SetThumbnail(opts.Thumbnail)
}
if opts.Original != nil {
snapshot.SetOriginal(opts.Original)
}
if opts.Preview != nil {
snapshot.SetPreview(opts.Preview)
}
if opts.Text != nil {
snapshot.SetText(opts.Text)
}
if opts.Status != "" {
snapshot.SetStatus(opts.Status)
}
if db := repo.db.Save(&snapshot); db.Error != nil {
return db.Error
}
return nil
}
func (repo *snapshotRepo) MapWithFile(id string, fileID string) error {
if db := repo.db.Exec("INSERT INTO snapshot_file (snapshot_id, file_id) VALUES (?, ?)", id, fileID); db.Error != nil {
return db.Error
}
return nil
}
func (repo *snapshotRepo) DeleteMappingsForFile(fileID string) error {
if db := repo.db.Exec("DELETE FROM snapshot_file WHERE file_id = ?", fileID); db.Error != nil {
return db.Error
}
return nil
}
func (repo *snapshotRepo) findAllForFile(fileID string) ([]*snapshotEntity, error) {
var res []*snapshotEntity
db := repo.db.
Raw("SELECT * FROM snapshot s LEFT JOIN snapshot_file sf ON s.id = sf.snapshot_id WHERE sf.file_id = ? ORDER BY s.version", fileID).
Scan(&res)
if db.Error != nil {
return nil, db.Error
}
return res, nil
}
func (repo *snapshotRepo) FindAllDangling() ([]model.Snapshot, error) {
var snapshots []*snapshotEntity
db := repo.db.Raw("SELECT * FROM snapshot s LEFT JOIN snapshot_file sf ON s.id = sf.snapshot_id WHERE sf.snapshot_id IS NULL").Scan(&snapshots)
if db.Error != nil {
return nil, db.Error
}
var res []model.Snapshot
for _, s := range snapshots {
res = append(res, s)
}
return res, nil
}
func (repo *snapshotRepo) DeleteAllDangling() error {
if db := repo.db.Exec("DELETE FROM snapshot WHERE id IN (SELECT s.id FROM (SELECT * FROM snapshot) s LEFT JOIN snapshot_file sf ON s.id = sf.snapshot_id WHERE sf.snapshot_id IS NULL)"); db.Error != nil {
return db.Error
}
return nil
}
func (repo *snapshotRepo) GetLatestVersionForFile(fileID string) (int64, error) {
type Result struct {
Result int64
}
var res Result
if db := repo.db.
Raw("SELECT coalesce(max(s.version), 0) + 1 result FROM snapshot s LEFT JOIN snapshot_file map ON s.id = map.snapshot_id WHERE map.file_id = ?", fileID).
Scan(&res); db.Error != nil {
return 0, db.Error
}
return res.Result, nil
}

View File

@@ -0,0 +1,121 @@
package repo
import (
"errors"
"voltaserve/errorpkg"
"voltaserve/infra"
"voltaserve/model"
"gorm.io/gorm"
)
type UserRepo interface {
Find(id string) (model.User, error)
FindByEmail(email string) (model.User, error)
FindAll() ([]model.User, error)
}
func NewUserRepo() UserRepo {
return newUserRepo()
}
func NewUser() model.User {
return &userEntity{}
}
type userEntity struct {
ID string `json:"id" gorm:"column:id"`
FullName string `json:"fullName" gorm:"column:full_name"`
Username string `json:"username" gorm:"column:username"`
Email string `json:"email" gorm:"column:email"`
Picture *string `json:"picture" gorm:"column:picture"`
IsEmailConfirmed bool `json:"isEmailConfirmed" gorm:"column:is_email_confirmed"`
PasswordHash string `json:"passwordHash" gorm:"column:password_hash"`
RefreshTokenValue *string `json:"refreshTokenValue" gorm:"column:refresh_token_value"`
RefreshTokenValidTo *int64 `json:"refreshTokenValidTo" gorm:"column:refresh_token_valid_to"`
ResetPasswordToken *string `json:"resetPasswordToken" gorm:"column:reset_password_token"`
EmailConfirmationToken *string `json:"emailConfirmationToken" gorm:"column:email_confirmation_token"`
CreateTime string `json:"createTime" gorm:"column:create_time"`
UpdateTime *string `json:"updateTime" gorm:"column:update_time"`
}
func (userEntity) TableName() string {
return "user"
}
func (u userEntity) GetID() string {
return u.ID
}
func (u userEntity) GetFullName() string {
return u.FullName
}
func (u userEntity) GetUsername() string {
return u.Username
}
func (u userEntity) GetEmail() string {
return u.Email
}
func (u userEntity) GetPicture() *string {
return u.Picture
}
func (u userEntity) GetIsEmailConfirmed() bool {
return u.IsEmailConfirmed
}
func (u userEntity) GetCreateTime() string {
return u.CreateTime
}
func (u userEntity) GetUpdateTime() *string {
return u.UpdateTime
}
type userRepo struct {
db *gorm.DB
}
func newUserRepo() *userRepo {
return &userRepo{
db: infra.GetDb(),
}
}
func (repo *userRepo) Find(id string) (model.User, error) {
var res = userEntity{}
db := repo.db.Where("id = ?", id).First(&res)
if db.Error != nil {
if errors.Is(db.Error, gorm.ErrRecordNotFound) {
return nil, errorpkg.NewUserNotFoundError(db.Error)
} else {
return nil, errorpkg.NewInternalServerError(db.Error)
}
}
return &res, nil
}
func (repo *userRepo) FindByEmail(email string) (model.User, error) {
var res = userEntity{}
db := repo.db.Where("email = ?", email).First(&res)
if db.Error != nil {
return nil, db.Error
}
return &res, nil
}
func (repo *userRepo) FindAll() ([]model.User, error) {
var entities []*userEntity
db := repo.db.Raw(`select * from "user"`).Scan(&entities)
if db.Error != nil {
return nil, db.Error
}
var res []model.User
for _, u := range entities {
res = append(res, u)
}
return res, nil
}

View File

@@ -0,0 +1,319 @@
package repo
import (
"errors"
"time"
"voltaserve/errorpkg"
"voltaserve/helper"
"voltaserve/infra"
"voltaserve/model"
"gorm.io/gorm"
)
type WorkspaceInsertOptions struct {
ID string
Name string
StorageCapacity int64
Image *string
OrganizationID string
RootID string
Bucket string
}
type WorkspaceRepo interface {
Insert(opts WorkspaceInsertOptions) (model.Workspace, error)
Find(id string) (model.Workspace, error)
UpdateName(id string, name string) (model.Workspace, error)
UpdateStorageCapacity(id string, storageCapacity int64) (model.Workspace, error)
UpdateRootID(id string, rootNodeID string) error
Delete(id string) error
GetIDs() ([]string, error)
GetIDsByOrganization(orgID string) ([]string, error)
GrantUserPermission(id string, userID string, permission string) error
}
func NewWorkspaceRepo() WorkspaceRepo {
return newWorkspaceRepo()
}
func NewWorkspace() model.Workspace {
return &workspaceEntity{}
}
type workspaceEntity struct {
ID string `json:"id," gorm:"column:id;size:36"`
Name string `json:"name" gorm:"column:name;size:255"`
StorageCapacity int64 `json:"storageCapacity" gorm:"column:storage_capacity"`
RootID string `json:"rootId" gorm:"column:root_id;size:36"`
OrganizationID string `json:"organizationId" gorm:"column:organization_id;size:36"`
UserPermissions []*userPermissionValue `json:"userPermissions" gorm:"-"`
GroupPermissions []*groupPermissionValue `json:"groupPermissions" gorm:"-"`
Bucket string `json:"bucket" gorm:"column:bucket;size:255"`
CreateTime string `json:"createTime" gorm:"column:create_time"`
UpdateTime *string `json:"updateTime,omitempty" gorm:"column:update_time"`
}
func (*workspaceEntity) TableName() string {
return "workspace"
}
func (w *workspaceEntity) BeforeCreate(*gorm.DB) (err error) {
w.CreateTime = time.Now().UTC().Format(time.RFC3339)
return nil
}
func (w *workspaceEntity) BeforeSave(*gorm.DB) (err error) {
timeNow := time.Now().UTC().Format(time.RFC3339)
w.UpdateTime = &timeNow
return nil
}
func (w *workspaceEntity) GetID() string {
return w.ID
}
func (w *workspaceEntity) GetName() string {
return w.Name
}
func (w *workspaceEntity) GetStorageCapacity() int64 {
return w.StorageCapacity
}
func (w *workspaceEntity) GetRootID() string {
return w.RootID
}
func (w *workspaceEntity) GetOrganizationID() string {
return w.OrganizationID
}
func (w *workspaceEntity) GetUserPermissions() []model.CoreUserPermission {
var res []model.CoreUserPermission
for _, p := range w.UserPermissions {
res = append(res, p)
}
return res
}
func (w *workspaceEntity) GetGroupPermissions() []model.CoreGroupPermission {
var res []model.CoreGroupPermission
for _, p := range w.GroupPermissions {
res = append(res, p)
}
return res
}
func (w *workspaceEntity) GetBucket() string {
return w.Bucket
}
func (w *workspaceEntity) GetCreateTime() string {
return w.CreateTime
}
func (w *workspaceEntity) GetUpdateTime() *string {
return w.UpdateTime
}
func (w *workspaceEntity) SetName(name string) {
w.Name = name
}
func (w *workspaceEntity) SetUpdateTime(updateTime *string) {
w.UpdateTime = updateTime
}
type workspaceRepo struct {
db *gorm.DB
permissionRepo *permissionRepo
}
func newWorkspaceRepo() *workspaceRepo {
return &workspaceRepo{
db: infra.GetDb(),
permissionRepo: newPermissionRepo(),
}
}
func (repo *workspaceRepo) Insert(opts WorkspaceInsertOptions) (model.Workspace, error) {
var id string
if len(opts.ID) > 0 {
id = opts.ID
} else {
id = helper.NewID()
}
workspace := workspaceEntity{
ID: id,
Name: opts.Name,
StorageCapacity: opts.StorageCapacity,
RootID: opts.RootID,
OrganizationID: opts.OrganizationID,
Bucket: opts.Bucket,
}
if db := repo.db.Save(&workspace); db.Error != nil {
return nil, db.Error
}
res, err := repo.find(id)
if err != nil {
return nil, err
}
if err := repo.populateModelFields([]*workspaceEntity{res}); err != nil {
return nil, err
}
return res, nil
}
func (repo *workspaceRepo) find(id string) (*workspaceEntity, error) {
var res = workspaceEntity{}
db := repo.db.Where("id = ?", id).First(&res)
if db.Error != nil {
if errors.Is(db.Error, gorm.ErrRecordNotFound) {
return nil, errorpkg.NewWorkspaceNotFoundError(db.Error)
} else {
return nil, errorpkg.NewInternalServerError(db.Error)
}
}
return &res, nil
}
func (repo *workspaceRepo) Find(id string) (model.Workspace, error) {
workspace, err := repo.find(id)
if err != nil {
return nil, err
}
if err := repo.populateModelFields([]*workspaceEntity{workspace}); err != nil {
return nil, err
}
return workspace, err
}
func (repo *workspaceRepo) UpdateName(id string, name string) (model.Workspace, error) {
workspace, err := repo.find(id)
if err != nil {
return &workspaceEntity{}, err
}
workspace.Name = name
if db := repo.db.Save(&workspace); db.Error != nil {
return nil, db.Error
}
res, err := repo.Find(id)
if err != nil {
return nil, err
}
return res, nil
}
func (repo *workspaceRepo) UpdateStorageCapacity(id string, storageCapacity int64) (model.Workspace, error) {
workspace, err := repo.find(id)
if err != nil {
return &workspaceEntity{}, err
}
workspace.StorageCapacity = storageCapacity
db := repo.db.Save(&workspace)
if db.Error != nil {
return nil, db.Error
}
res, err := repo.Find(id)
if err != nil {
return nil, err
}
return res, nil
}
func (repo *workspaceRepo) UpdateRootID(id string, rootNodeID string) error {
db := repo.db.Exec("UPDATE workspace SET root_id = ? WHERE id = ?", rootNodeID, id)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *workspaceRepo) Delete(id string) error {
db := repo.db.Exec("DELETE FROM workspace WHERE id = ?", id)
if db.Error != nil {
return db.Error
}
db = repo.db.Exec("DELETE FROM userpermission WHERE resource_id = ?", id)
if db.Error != nil {
return db.Error
}
db = repo.db.Exec("DELETE FROM grouppermission WHERE resource_id = ?", id)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *workspaceRepo) GetIDs() ([]string, error) {
type IDResult struct {
Result string
}
var ids []IDResult
db := repo.db.Raw("SELECT id result FROM workspace ORDER BY create_time DESC").Scan(&ids)
if db.Error != nil {
return []string{}, db.Error
}
res := []string{}
for _, id := range ids {
res = append(res, id.Result)
}
return res, nil
}
func (repo *workspaceRepo) GetIDsByOrganization(orgID string) ([]string, error) {
type IDResult struct {
Result string
}
var ids []IDResult
db := repo.db.
Raw("SELECT id result FROM workspace WHERE organization_id = ? ORDER BY create_time DESC", orgID).
Scan(&ids)
if db.Error != nil {
return nil, db.Error
}
res := []string{}
for _, id := range ids {
res = append(res, id.Result)
}
return res, nil
}
func (repo *workspaceRepo) GrantUserPermission(id string, userID string, permission string) error {
db := repo.db.Exec(
"INSERT INTO userpermission (id, user_id, resource_id, permission) VALUES (?, ?, ?, ?) ON CONFLICT (user_id, resource_id) DO UPDATE SET permission = ?",
helper.NewID(), userID, id, permission, permission)
if db.Error != nil {
return db.Error
}
return nil
}
func (repo *workspaceRepo) populateModelFields(workspaces []*workspaceEntity) error {
for _, w := range workspaces {
w.UserPermissions = make([]*userPermissionValue, 0)
userPermissions, err := repo.permissionRepo.GetUserPermissions(w.ID)
if err != nil {
return err
}
for _, p := range userPermissions {
w.UserPermissions = append(w.UserPermissions, &userPermissionValue{
UserID: p.UserID,
Value: p.Permission,
})
}
w.GroupPermissions = make([]*groupPermissionValue, 0)
groupPermissions, err := repo.permissionRepo.GetGroupPermissions(w.ID)
if err != nil {
return err
}
for _, p := range groupPermissions {
w.GroupPermissions = append(w.GroupPermissions, &groupPermissionValue{
GroupID: p.GroupID,
Value: p.Permission,
})
}
}
return nil
}

View File

@@ -0,0 +1,11 @@
package router
const (
FileDefaultPageSize = 100
WorkspaceDefaultPageSize = 100
OrganizationDefaultPageSize = 100
InvitationDefaultPageSize = 100
GroupDefaultPageSize = 100
UserDefaultPageSize = 100
OCRLanguageDefaultPageSize = 100
)

View File

@@ -0,0 +1,927 @@
package router
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"voltaserve/config"
"voltaserve/errorpkg"
"voltaserve/helper"
"voltaserve/infra"
"voltaserve/model"
"voltaserve/service"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
type FileRouter struct {
fileSvc *service.FileService
workspaceSvc *service.WorkspaceService
config config.Config
}
func NewFileRouter() *FileRouter {
return &FileRouter{
fileSvc: service.NewFileService(),
workspaceSvc: service.NewWorkspaceService(),
config: config.GetConfig(),
}
}
func (r *FileRouter) AppendRoutes(g fiber.Router) {
g.Post("/", r.Upload)
g.Post("/create_folder", r.CreateFolder)
g.Get("/list", r.ListByPath)
g.Get("/get", r.GetByPath)
g.Post("/batch_delete", r.BatchDelete)
g.Post("/batch_get", r.BatchGet)
g.Get("/:id", r.GetByID)
g.Patch("/:id", r.Patch)
g.Delete("/:id", r.Delete)
g.Get("/:id/list", r.List)
g.Get("/:id/get_item_count", r.GetItemCount)
g.Get("/:id/get_path", r.GetPath)
g.Get("/:id/get_ids", r.GetIDs)
g.Post("/:id/move", r.Move)
g.Post("/:id/rename", r.Rename)
g.Post("/:id/copy", r.Copy)
g.Get("/:id/get_size", r.GetSize)
g.Post("/grant_user_permission", r.GrantUserPermission)
g.Post("/revoke_user_permission", r.RevokeUserPermission)
g.Post("/grant_group_permission", r.GrantGroupPermission)
g.Post("/revoke_group_permission", r.RevokeGroupPermission)
g.Get("/:id/get_user_permissions", r.GetUserPermissions)
g.Get("/:id/get_group_permissions", r.GetGroupPermissions)
}
// Upload godoc
//
// @Summary Upload
// @Description Upload
// @Tags Files
// @Id files_upload
// @Accept x-www-form-urlencoded
// @Produce json
// @Param workspace_id query string true "Workspace ID"
// @Param parent_id query string false "Parent ID"
// @Param name query string false "Name"
// @Success 200 {object} service.File
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files [post]
func (r *FileRouter) Upload(c *fiber.Ctx) error {
userID := GetUserID(c)
workspaceID := c.Query("workspace_id")
if workspaceID == "" {
return errorpkg.NewMissingQueryParamError("workspace_id")
}
parentID := c.Query("parent_id")
if parentID == "" {
workspace, err := r.workspaceSvc.Find(workspaceID, userID)
if err != nil {
return err
}
parentID = workspace.RootID
}
fh, err := c.FormFile("file")
if err != nil {
return err
}
ok, err := r.workspaceSvc.HasEnoughSpaceForByteSize(workspaceID, fh.Size)
if err != nil {
return err
}
if !ok {
return errorpkg.NewStorageLimitExceededError()
}
name := c.Query("name")
if name == "" {
name = fh.Filename
}
file, err := r.fileSvc.Create(service.FileCreateOptions{
Name: name,
Type: model.FileTypeFile,
ParentID: &parentID,
WorkspaceID: workspaceID,
}, userID)
if err != nil {
return err
}
path := filepath.FromSlash(os.TempDir() + "/" + helper.NewID() + filepath.Ext(fh.Filename))
if err := c.SaveFile(fh, path); err != nil {
return err
}
defer func(name string) {
if err := os.Remove(name); err != nil {
log.Error(err)
}
}(path)
file, err = r.fileSvc.Store(file.ID, path, userID)
if err != nil {
return err
}
return c.Status(http.StatusCreated).JSON(file)
}
// Patch godoc
//
// @Summary Patch
// @Description Patch
// @Tags Files
// @Id files_patch
// @Accept x-www-form-urlencoded
// @Produce json
// @Param id path string true "ID"
// @Success 200 {object} service.File
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id} [patch]
func (r *FileRouter) Patch(c *fiber.Ctx) error {
userID := GetUserID(c)
files, err := r.fileSvc.FindByID([]string{c.Params("id")}, userID)
if err != nil {
return err
}
file := files[0]
fh, err := c.FormFile("file")
if err != nil {
return err
}
ok, err := r.workspaceSvc.HasEnoughSpaceForByteSize(file.WorkspaceID, fh.Size)
if err != nil {
return err
}
if !ok {
return errorpkg.NewStorageLimitExceededError()
}
path := filepath.FromSlash(os.TempDir() + "/" + helper.NewID() + filepath.Ext(fh.Filename))
if err := c.SaveFile(fh, path); err != nil {
return err
}
defer func(name string) {
if err := os.Remove(name); err != nil {
log.Error(err)
}
}(path)
file, err = r.fileSvc.Store(file.ID, path, userID)
if err != nil {
return err
}
return c.JSON(file)
}
// CreateFolder godoc
//
// @Summary Create
// @Description Create
// @Tags Files
// @Id files_create_folder
// @Accept json
// @Produce json
// @Param body body service.FileCreateFolderOptions true "Body"
// @Success 200 {object} service.File
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/create_folder [post]
func (r *FileRouter) CreateFolder(c *fiber.Ctx) error {
userID := GetUserID(c)
opts := new(service.FileCreateFolderOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
if err := validator.New().Struct(opts); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
parentID := opts.ParentID
if parentID == nil {
workspace, err := r.workspaceSvc.Find(opts.WorkspaceID, userID)
if err != nil {
return err
}
parentID = &workspace.RootID
}
res, err := r.fileSvc.Create(service.FileCreateOptions{
Name: opts.Name,
Type: model.FileTypeFolder,
ParentID: parentID,
WorkspaceID: opts.WorkspaceID,
}, userID)
if err != nil {
return err
}
return c.Status(http.StatusCreated).JSON(res)
}
// GetByID godoc
//
// @Summary Get by ID
// @Description Get by ID
// @Tags Files
// @Id files_get_by_id
// @Produce json
// @Param id path string true "ID"
// @Success 200 {object} service.File
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id} [get]
func (r *FileRouter) GetByID(c *fiber.Ctx) error {
userID := GetUserID(c)
res, err := r.fileSvc.FindByID([]string{c.Params("id")}, userID)
if err != nil {
return err
}
if len(res) == 0 {
return errorpkg.NewFileNotFoundError(nil)
}
return c.JSON(res[0])
}
// GetByPath godoc
//
// @Summary Get by Path
// @Description Get by Path
// @Tags Files
// @Id files_get_by_path
// @Produce json
// @Param id path string true "ID"
// @Success 200 {object} service.File
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/get [get]
func (r *FileRouter) GetByPath(c *fiber.Ctx) error {
userID := GetUserID(c)
if c.Query("path") == "" {
return errorpkg.NewMissingQueryParamError("path")
}
res, err := r.fileSvc.FindByPath(c.Query("path"), userID)
if err != nil {
return err
}
return c.JSON(res)
}
// ListByPath godoc
//
// @Summary List by Path
// @Description List by Path
// @Tags Files
// @Id files_list_by_path
// @Produce json
// @Param path query string true "Path"
// @Success 200 {array} service.File
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/list [get]
func (r *FileRouter) ListByPath(c *fiber.Ctx) error {
userID := GetUserID(c)
if c.Query("path") == "" {
return errorpkg.NewMissingQueryParamError("path")
}
res, err := r.fileSvc.ListByPath(c.Query("path"), userID)
if err != nil {
return err
}
return c.JSON(res)
}
// List godoc
//
// @Summary List
// @Description List
// @Tags Files
// @Id files_list
// @Produce json
// @Param id path string true "ID"
// @Param type query string false "Type"
// @Param page query string false "Page"
// @Param size query string false "Size"
// @Param sort_by query string false "Sort By"
// @Param sort_order query string false "Sort Order"
// @Param query query string false "Query"
// @Success 200 {object} service.FileList
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/list [get]
func (r *FileRouter) List(c *fiber.Ctx) error {
var err error
var res *service.FileList
id := c.Params("id")
userID := GetUserID(c)
var page int64
if c.Query("page") == "" {
page = 1
} else {
page, err = strconv.ParseInt(c.Query("page"), 10, 32)
if err != nil {
page = 1
}
}
var size int64
if c.Query("size") == "" {
size = FileDefaultPageSize
} else {
size, err = strconv.ParseInt(c.Query("size"), 10, 32)
if err != nil {
return err
}
}
sortBy := c.Query("sort_by")
if !IsValidSortBy(sortBy) {
return errorpkg.NewInvalidQueryParamError("sort_by")
}
sortOrder := c.Query("sort_order")
if !IsValidSortOrder(sortOrder) {
return errorpkg.NewInvalidQueryParamError("sort_order")
}
fileType := c.Query("type")
if fileType != model.FileTypeFile && fileType != model.FileTypeFolder && fileType != "" {
return errorpkg.NewInvalidQueryParamError("type")
}
query := c.Query("query")
opts := service.FileListOptions{
Page: uint(page),
Size: uint(size),
SortBy: sortBy,
SortOrder: sortOrder,
}
if query != "" {
bytes, err := base64.StdEncoding.DecodeString(query + strings.Repeat("=", (4-len(query)%4)%4))
if err != nil {
return errorpkg.NewInvalidQueryParamError("query")
}
if err := json.Unmarshal(bytes, &opts.Query); err != nil {
return errorpkg.NewInvalidQueryParamError("query")
}
res, err = r.fileSvc.Search(id, opts, userID)
if err != nil {
return err
}
} else {
if fileType != "" {
opts.Query = &service.FileQuery{
Type: &fileType,
}
}
res, err = r.fileSvc.List(id, opts, userID)
if err != nil {
return err
}
}
return c.JSON(res)
}
// GetIDs godoc
//
// @Summary Get IDs
// @Description Get IDs
// @Tags Files
// @Id files_get_ids
// @Produce json
// @Param id path string true "ID"
// @Success 200 {array} string
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/get_ids [get]
func (r *FileRouter) GetIDs(c *fiber.Ctx) error {
userID := GetUserID(c)
res, err := r.fileSvc.GetIDs(c.Params("id"), userID)
if err != nil {
return err
}
return c.JSON(res)
}
// GetPath godoc
//
// @Summary Get path
// @Description Get path
// @Tags Files
// @Id files_get_path
// @Produce json
// @Param id path string true "ID"
// @Success 200 {array} service.File
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/get_path [get]
func (r *FileRouter) GetPath(c *fiber.Ctx) error {
userID := GetUserID(c)
res, err := r.fileSvc.GetPath(c.Params("id"), userID)
if err != nil {
return err
}
return c.JSON(res)
}
// Copy godoc
//
// @Summary Copy
// @Description Copy
// @Tags Files
// @Id files_copy
// @Produce json
// @Param id path string true "ID"
// @Param body body service.FileCopyOptions true "Body"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/copy [post]
func (r *FileRouter) Copy(c *fiber.Ctx) error {
userID := GetUserID(c)
opts := new(service.FileCopyOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
if err := validator.New().Struct(opts); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
res, err := r.fileSvc.Copy(c.Params("id"), opts.IDs, userID)
if err != nil {
return err
}
return c.JSON(res)
}
// Move godoc
//
// @Summary Move
// @Description Move
// @Tags Files
// @Id files_move
// @Produce json
// @Param id path string true "ID"
// @Param body body service.FileMoveOptions true "Body"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/move [post]
func (r *FileRouter) Move(c *fiber.Ctx) error {
userID := GetUserID(c)
opts := new(service.FileMoveOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
if err := validator.New().Struct(opts); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
if _, err := r.fileSvc.Move(c.Params("id"), opts.IDs, userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}
// Rename godoc
//
// @Summary Rename
// @Description Rename
// @Tags Files
// @Id files_rename
// @Produce json
// @Param id path string true "ID"
// @Param body body service.FileRenameOptions true "Body"
// @Success 200 {object} service.File
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/rename [post]
func (r *FileRouter) Rename(c *fiber.Ctx) error {
userID := GetUserID(c)
opts := new(service.FileRenameOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
if err := validator.New().Struct(opts); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
res, err := r.fileSvc.Rename(c.Params("id"), opts.Name, userID)
if err != nil {
return err
}
return c.JSON(res)
}
// Delete godoc
//
// @Summary Delete
// @Description Delete
// @Tags Files
// @Id files_delete
// @Produce json
// @Param id path string true "ID"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id} [delete]
func (r *FileRouter) Delete(c *fiber.Ctx) error {
userID := GetUserID(c)
_, err := r.fileSvc.Delete([]string{c.Params("id")}, userID)
if err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}
// BatchGet godoc
//
// @Summary Batch get
// @Description Batch get
// @Tags Files
// @Id files_batch_get
// @Produce json
// @Param body body service.FileBatchGetOptions true "Body"
// @Success 200 {array} service.File
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/batch_get [post]
func (r *FileRouter) BatchGet(c *fiber.Ctx) error {
userID := GetUserID(c)
opts := new(service.FileBatchGetOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
if err := validator.New().Struct(opts); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
res, err := r.fileSvc.FindByID(opts.IDs, userID)
if err != nil {
return err
}
return c.JSON(res)
}
// BatchDelete godoc
//
// @Summary Batch delete
// @Description Batch delete
// @Tags Files
// @Id files_batch_delete
// @Produce json
// @Param body body service.FileBatchDeleteOptions true "Body"
// @Success 200 {array} string
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/batch_delete [post]
func (r *FileRouter) BatchDelete(c *fiber.Ctx) error {
userID := GetUserID(c)
opts := new(service.FileBatchDeleteOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
if err := validator.New().Struct(opts); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
res, err := r.fileSvc.Delete(opts.IDs, userID)
if err != nil {
return err
}
return c.JSON(res)
}
// GetSize godoc
//
// @Summary Get size
// @Description Get size
// @Tags Files
// @Id files_get_size
// @Produce json
// @Param id path string true "ID"
// @Success 200 {object} int
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/get_size [get]
func (r *FileRouter) GetSize(c *fiber.Ctx) error {
userID := GetUserID(c)
id := c.Params("id")
res, err := r.fileSvc.GetSize(id, userID)
if err != nil {
return err
}
return c.JSON(res)
}
// GetItemCount godoc
//
// @Summary Get children count
// @Description Get children count
// @Tags Files
// @Id files_get_children_count
// @Produce json
// @Param id path string true "ID"
// @Success 200 {object} int
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/get_item_count [get]
func (r *FileRouter) GetItemCount(c *fiber.Ctx) error {
userID := GetUserID(c)
res, err := r.fileSvc.GetItemCount(c.Params("id"), userID)
if err != nil {
return err
}
return c.JSON(res)
}
// GrantUserPermission godoc
//
// @Summary Grant user permission
// @Description Grant user permission
// @Tags Files
// @Id files_grant_user_permission
// @Produce json
// @Param id path string true "ID"
// @Param body body service.FileGrantUserPermissionOptions true "Body"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/grant_user_permission [post]
func (r *FileRouter) GrantUserPermission(c *fiber.Ctx) error {
userID := GetUserID(c)
opts := new(service.FileGrantUserPermissionOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
if err := validator.New().Struct(opts); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
if err := r.fileSvc.GrantUserPermission(opts.IDs, opts.UserID, opts.Permission, userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}
// RevokeUserPermission godoc
//
// @Summary Revoke user permission
// @Description Revoke user permission
// @Tags Files
// @Id files_revoke_user_permission
// @Produce json
// @Param id path string true "ID"
// @Param body body service.FileRevokeUserPermissionOptions true "Body"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/revoke_user_permission [post]
func (r *FileRouter) RevokeUserPermission(c *fiber.Ctx) error {
userID := GetUserID(c)
opts := new(service.FileRevokeUserPermissionOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
if err := validator.New().Struct(opts); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
if err := r.fileSvc.RevokeUserPermission(opts.IDs, opts.UserID, userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}
// GrantGroupPermission godoc
//
// @Summary Grant group permission
// @Description Grant group permission
// @Tags Files
// @Id files_grant_group_permission
// @Produce json
// @Param id path string true "ID"
// @Param body body service.FileGrantGroupPermissionOptions true "Body"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/grant_group_permission [post]
func (r *FileRouter) GrantGroupPermission(c *fiber.Ctx) error {
userID := GetUserID(c)
opts := new(service.FileGrantGroupPermissionOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
if err := validator.New().Struct(opts); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
if err := r.fileSvc.GrantGroupPermission(opts.IDs, opts.GroupID, opts.Permission, userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}
// RevokeGroupPermission godoc
//
// @Summary Revoke group permission
// @Description Revoke group permission
// @Tags Files
// @Id files_revoke_group_permission
// @Produce json
// @Param id path string true "ID"
// @Param body body service.FileRevokeGroupPermissionOptions true "Body"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/revoke_group_permission [post]
func (r *FileRouter) RevokeGroupPermission(c *fiber.Ctx) error {
userID := GetUserID(c)
opts := new(service.FileRevokeGroupPermissionOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
if err := validator.New().Struct(opts); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
if err := r.fileSvc.RevokeGroupPermission(opts.IDs, opts.GroupID, userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}
// GetUserPermissions godoc
//
// @Summary Get user permissions
// @Description Get user permissions
// @Tags Files
// @Id files_get_user_permissions
// @Produce json
// @Param id path string true "ID"
// @Success 200 {array} service.UserPermission
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/get_user_permissions [get]
func (r *FileRouter) GetUserPermissions(c *fiber.Ctx) error {
userID := GetUserID(c)
res, err := r.fileSvc.GetUserPermissions(c.Params("id"), userID)
if err != nil {
return err
}
return c.JSON(res)
}
// GetGroupPermissions godoc
//
// @Summary Get group permissions
// @Description Get group permissions
// @Tags Files
// @Id files_get_group_permissions
// @Produce json
// @Param id path string true "ID"
// @Success 200 {array} service.GroupPermission
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/get_group_permissions [get]
func (r *FileRouter) GetGroupPermissions(c *fiber.Ctx) error {
userID := GetUserID(c)
res, err := r.fileSvc.GetGroupPermissions(c.Params("id"), userID)
if err != nil {
return err
}
return c.JSON(res)
}
type FileDownloadRouter struct {
fileSvc *service.FileService
accessTokenCookieName string
}
func NewFileDownloadRouter() *FileDownloadRouter {
return &FileDownloadRouter{
fileSvc: service.NewFileService(),
accessTokenCookieName: "voltaserve_access_token",
}
}
func (r *FileDownloadRouter) AppendNonJWTRoutes(g fiber.Router) {
g.Get("/:id/original:ext", r.DownloadOriginal)
g.Get("/:id/preview:ext", r.DownloadPreview)
}
// DownloadOriginal godoc
//
// @Summary Download original
// @Description Download original
// @Tags Files
// @Id files_download_original
// @Produce json
// @Param id path string true "ID"
// @Param access_token query string true "Access Token"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/original{ext} [get]
func (r *FileDownloadRouter) DownloadOriginal(c *fiber.Ctx) error {
accessToken := c.Cookies(r.accessTokenCookieName)
if accessToken == "" {
accessToken = c.Query("access_token")
if accessToken == "" {
return errorpkg.NewFileNotFoundError(nil)
}
}
userID, err := r.getUserID(accessToken)
if err != nil {
return c.SendStatus(http.StatusNotFound)
}
buf, file, snapshot, err := r.fileSvc.DownloadOriginalBuffer(c.Params("id"), userID)
if err != nil {
return err
}
if filepath.Ext(snapshot.GetOriginal().Key) != c.Params("ext") {
return errorpkg.NewS3ObjectNotFoundError(nil)
}
bytes := buf.Bytes()
c.Set("Content-Type", infra.DetectMimeFromBytes(bytes))
c.Set("Content-Disposition", fmt.Sprintf("filename=\"%s\"", file.GetName()))
return c.Send(bytes)
}
// DownloadPreview godoc
//
// @Summary Download preview
// @Description Download preview
// @Tags Files
// @Id files_download_preview
// @Produce json
// @Param id path string true "ID"
// @Param access_token query string true "Access Token"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/{id}/preview{ext} [get]
func (r *FileDownloadRouter) DownloadPreview(c *fiber.Ctx) error {
accessToken := c.Cookies(r.accessTokenCookieName)
if accessToken == "" {
accessToken = c.Query("access_token")
if accessToken == "" {
return errorpkg.NewFileNotFoundError(nil)
}
}
userID, err := r.getUserID(accessToken)
if err != nil {
return c.SendStatus(http.StatusNotFound)
}
buf, file, snapshot, err := r.fileSvc.DownloadPreviewBuffer(c.Params("id"), userID)
if err != nil {
return err
}
if filepath.Ext(snapshot.GetPreview().Key) != c.Params("ext") {
return errorpkg.NewS3ObjectNotFoundError(nil)
}
bytes := buf.Bytes()
c.Set("Content-Type", infra.DetectMimeFromBytes(bytes))
c.Set("Content-Disposition", fmt.Sprintf("filename=\"%s\"", file.GetName()))
return c.Send(bytes)
}
func (r *FileDownloadRouter) getUserID(accessToken string) (string, error) {
token, err := jwt.Parse(accessToken, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(config.GetConfig().Security.JWTSigningKey), nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims["sub"].(string), nil
} else {
return "", errors.New("cannot find sub claim")
}
}
type ConversionWebhookRouter struct {
fileSvc *service.FileService
}
func NewConversionWebhookRouter() *ConversionWebhookRouter {
return &ConversionWebhookRouter{
fileSvc: service.NewFileService(),
}
}
func (r *ConversionWebhookRouter) AppendInternalRoutes(g fiber.Router) {
g.Post("/conversion_webhook/update_snapshot", r.UpdateSnapshot)
}
// UpdateSnapshot godoc
//
// @Summary Update snapshot
// @Description Update snapshot
// @Tags Files
// @Id files_conversion_webhook_update_snapshot
// @Produce json
// @Param body body service.SnapshotUpdateOptions true "Body"
// @Success 201
// @Failure 401 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /files/conversion_webhook/update_snapshot [post]
func (r *ConversionWebhookRouter) UpdateSnapshot(c *fiber.Ctx) error {
apiKey := c.Query("api_key")
if apiKey == "" {
return errorpkg.NewMissingQueryParamError("api_key")
}
opts := new(service.SnapshotUpdateOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
if err := validator.New().Struct(opts); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
if err := r.fileSvc.UpdateSnapshot(*opts, apiKey); err != nil {
return err
}
return c.SendStatus(204)
}

View File

@@ -0,0 +1,249 @@
package router
import (
"net/http"
"strconv"
"voltaserve/errorpkg"
"voltaserve/service"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type GroupRouter struct {
groupSvc *service.GroupService
}
func NewGroupRouter() *GroupRouter {
return &GroupRouter{
groupSvc: service.NewGroupService(),
}
}
func (r *GroupRouter) AppendRoutes(g fiber.Router) {
g.Get("/", r.List)
g.Post("/", r.Create)
g.Get("/:id", r.GetByID)
g.Delete("/:id", r.Delete)
g.Post("/:id/update_name", r.UpdateName)
g.Post("/:id/add_member", r.AddMember)
g.Post("/:id/remove_member", r.RemoveMember)
}
// Create godoc
//
// @Summary Create
// @Description Create
// @Tags Groups
// @Id groups_create
// @Accept json
// @Produce json
// @Param body body service.GroupCreateOptions true "Body"
// @Success 200 {object} service.Group
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /groups [post]
func (r *GroupRouter) Create(c *fiber.Ctx) error {
userID := GetUserID(c)
req := new(service.GroupCreateOptions)
if err := c.BodyParser(req); err != nil {
return err
}
if err := validator.New().Struct(req); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
res, err := r.groupSvc.Create(*req, userID)
if err != nil {
return err
}
return c.Status(http.StatusCreated).JSON(res)
}
// GetByID godoc
//
// @Summary Get by ID
// @Description Get by ID
// @Tags Groups
// @Id groups_get_by_id
// @Produce json
// @Param id path string true "ID"
// @Success 200 {object} service.Group
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /groups/{id} [get]
func (r *GroupRouter) GetByID(c *fiber.Ctx) error {
userID := GetUserID(c)
res, err := r.groupSvc.Find(c.Params("id"), userID)
if err != nil {
return err
}
return c.JSON(res)
}
// List godoc
//
// @Summary List
// @Description List
// @Tags Groups
// @Id groups_list
// @Produce json
// @Param query query string false "Query"
// @Param organization_id query string false "Organization ID"
// @Param page query string false "Page"
// @Param size query string false "Size"
// @Param sort_by query string false "Sort By"
// @Param sort_order query string false "Sort Order"
// @Success 200 {object} service.GroupList
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /groups [get]
func (r *GroupRouter) List(c *fiber.Ctx) error {
var err error
var page int64
if c.Query("page") == "" {
page = 1
} else {
page, err = strconv.ParseInt(c.Query("page"), 10, 32)
if err != nil {
page = 1
}
}
var size int64
if c.Query("size") == "" {
size = GroupDefaultPageSize
} else {
size, err = strconv.ParseInt(c.Query("size"), 10, 32)
if err != nil {
return err
}
}
sortBy := c.Query("sort_by")
if !IsValidSortBy(sortBy) {
return errorpkg.NewInvalidQueryParamError("sort_by")
}
sortOrder := c.Query("sort_order")
if !IsValidSortOrder(sortOrder) {
return errorpkg.NewInvalidQueryParamError("sort_order")
}
res, err := r.groupSvc.List(service.GroupListOptions{
Query: c.Query("query"),
OrganizationID: c.Query("organization_id"),
Page: uint(page),
Size: uint(size),
SortBy: sortBy,
SortOrder: sortOrder,
}, GetUserID(c))
if err != nil {
return err
}
return c.JSON(res)
}
// UpdateName godoc
//
// @Summary Update name
// @Description Update name
// @Tags Groups
// @Id groups_update_name
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Param body body service.GroupUpdateNameOptions true "Body"
// @Success 200 {object} service.Group
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /groups/{id}/update_name [post]
func (r *GroupRouter) UpdateName(c *fiber.Ctx) error {
userID := GetUserID(c)
req := new(service.GroupUpdateNameOptions)
if err := c.BodyParser(req); err != nil {
return err
}
if err := validator.New().Struct(req); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
res, err := r.groupSvc.UpdateName(c.Params("id"), req.Name, userID)
if err != nil {
return err
}
return c.JSON(res)
}
// Delete godoc
//
// @Summary Delete
// @Description Delete
// @Tags Groups
// @Id groups_delete
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Success 200
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /groups/{id} [delete]
func (r *GroupRouter) Delete(c *fiber.Ctx) error {
userID := GetUserID(c)
if err := r.groupSvc.Delete(c.Params("id"), userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}
// AddMember godoc
//
// @Summary Add member
// @Description Add member
// @Tags Groups
// @Id groups_add_member
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /groups/{id}/add_member [post]
func (r *GroupRouter) AddMember(c *fiber.Ctx) error {
userID := GetUserID(c)
req := new(service.GroupAddMemberOptions)
if err := c.BodyParser(req); err != nil {
return err
}
if err := validator.New().Struct(req); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
if err := r.groupSvc.AddMember(c.Params("id"), req.UserID, userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}
// RemoveMember godoc
//
// @Summary Remove member
// @Description Remove member
// @Tags Groups
// @Id groups_remove_member
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Param body body service.GroupRemoveMemberOptions true "Body"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /groups/{id}/remove_member [post]
func (r *GroupRouter) RemoveMember(c *fiber.Ctx) error {
userID := GetUserID(c)
req := new(service.GroupRemoveMemberOptions)
if err := c.BodyParser(req); err != nil {
return err
}
if err := validator.New().Struct(req); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
if err := r.groupSvc.RemoveMember(c.Params("id"), req.UserID, userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}

View File

@@ -0,0 +1,257 @@
package router
import (
"net/http"
"strconv"
"voltaserve/errorpkg"
"voltaserve/service"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type InvitationRouter struct {
invitationSvc *service.InvitationService
}
func NewInvitationRouter() *InvitationRouter {
return &InvitationRouter{
invitationSvc: service.NewInvitationService(),
}
}
func (r *InvitationRouter) AppendRoutes(g fiber.Router) {
g.Post("/", r.Create)
g.Get("/get_incoming", r.GetIncoming)
g.Get("/get_outgoing", r.GetOutgoing)
g.Post("/:id/accept", r.Accept)
g.Post("/:id/resend", r.Resend)
g.Post("/:id/decline", r.Decline)
g.Delete("/:id", r.Delete)
}
// Create godoc
//
// @Summary Create
// @Description Create
// @Tags Invitations
// @Id invitations_create
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Param body body service.InvitationCreateOptions true "Body"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /invitations [post]
func (r *InvitationRouter) Create(c *fiber.Ctx) error {
userID := GetUserID(c)
req := new(service.InvitationCreateOptions)
if err := c.BodyParser(req); err != nil {
return err
}
if err := validator.New().Struct(req); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
if err := r.invitationSvc.Create(*req, userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}
// GetIncoming godoc
//
// @Summary Get incoming
// @Description Get incoming
// @Tags Invitations
// @Id invitation_get_incoming
// @Produce json
// @Param page query string false "Page"
// @Param size query string false "Size"
// @Param sort_by query string false "Sort By"
// @Param sort_order query string false "Sort Order"
// @Success 200 {object} service.InvitationList
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /invitations/get_incoming [get]
func (r *InvitationRouter) GetIncoming(c *fiber.Ctx) error {
var err error
var page int64
if c.Query("page") == "" {
page = 1
} else {
page, err = strconv.ParseInt(c.Query("page"), 10, 32)
if err != nil {
page = 1
}
}
var size int64
if c.Query("size") == "" {
size = InvitationDefaultPageSize
} else {
size, err = strconv.ParseInt(c.Query("size"), 10, 32)
if err != nil {
return err
}
}
sortBy := c.Query("sort_by")
if !IsValidSortBy(sortBy) {
return errorpkg.NewInvalidQueryParamError("sort_by")
}
sortOrder := c.Query("sort_order")
if !IsValidSortOrder(sortOrder) {
return errorpkg.NewInvalidQueryParamError("sort_order")
}
res, err := r.invitationSvc.GetIncoming(service.InvitationListOptions{
Page: uint(page),
Size: uint(size),
SortBy: sortBy,
SortOrder: sortOrder,
}, GetUserID(c))
if err != nil {
return err
}
return c.JSON(res)
}
// GetOutgoing godoc
//
// @Summary Get outgoing
// @Description Get outgoing
// @Tags Invitations
// @Id invitation_get_outgoing
// @Produce json
// @Param organization_id query string true "Organization ID"
// @Param page query string false "Page"
// @Param size query string false "Size"
// @Param sort_by query string false "Sort By"
// @Param sort_order query string false "Sort Order"
// @Success 200 {object} service.InvitationList
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /invitations/get_outgoing [get]
func (r *InvitationRouter) GetOutgoing(c *fiber.Ctx) error {
orgID := c.Query("organization_id")
if orgID == "" {
return errorpkg.NewMissingQueryParamError("org")
}
var err error
var page int64
if c.Query("page") == "" {
page = 1
} else {
page, err = strconv.ParseInt(c.Query("page"), 10, 32)
if err != nil {
page = 1
}
}
var size int64
if c.Query("size") == "" {
size = InvitationDefaultPageSize
} else {
size, err = strconv.ParseInt(c.Query("size"), 10, 32)
if err != nil {
return err
}
}
sortBy := c.Query("sort_by")
if !IsValidSortBy(sortBy) {
return errorpkg.NewInvalidQueryParamError("sort_by")
}
sortOrder := c.Query("sort_order")
if !IsValidSortOrder(sortOrder) {
return errorpkg.NewInvalidQueryParamError("sort_order")
}
res, err := r.invitationSvc.GetOutgoing(orgID, service.InvitationListOptions{
Page: uint(page),
Size: uint(size),
SortBy: sortBy,
SortOrder: sortOrder,
}, GetUserID(c))
if err != nil {
return err
}
return c.JSON(res)
}
// Delete godoc
//
// @Summary Delete
// @Description Delete
// @Tags Invitations
// @Id invitations_delete
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /invitations/{id} [delete]
func (r *InvitationRouter) Delete(c *fiber.Ctx) error {
userID := GetUserID(c)
if err := r.invitationSvc.Delete(c.Params("id"), userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}
// Resend godoc
//
// @Summary Resend
// @Description Resend
// @Tags Invitations
// @Id invitations_resend
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /invitations/{id}/resend [post]
func (r *InvitationRouter) Resend(c *fiber.Ctx) error {
userID := GetUserID(c)
if err := r.invitationSvc.Resend(c.Params("id"), userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}
// Accept godoc
//
// @Summary Accept
// @Description Accept
// @Tags Invitations
// @Id invitation_accept
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /invitations/{id}/accept [post]
func (r *InvitationRouter) Accept(c *fiber.Ctx) error {
userID := GetUserID(c)
if err := r.invitationSvc.Accept(c.Params("id"), userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}
// Decline godoc
//
// @Summary Delete
// @Description Delete
// @Tags Invitations
// @Id invitations_decline
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /invitations/{id}/decline [post]
func (r *InvitationRouter) Decline(c *fiber.Ctx) error {
userID := GetUserID(c)
if err := r.invitationSvc.Decline(c.Params("id"), userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}

View File

@@ -0,0 +1,40 @@
package router
import (
"voltaserve/service"
"github.com/gofiber/fiber/v2"
)
type NotificationRouter struct {
notificationSvc *service.NotificationService
}
func NewNotificationRouter() *NotificationRouter {
return &NotificationRouter{
notificationSvc: service.NewNotificationService(),
}
}
func (r *NotificationRouter) AppendRoutes(g fiber.Router) {
g.Get("/", r.GetAll)
}
// GetAll godoc
//
// @Summary Get notifications
// @Description Get notifications
// @Tags Notifications
// @Id notification_get_all
// @Produce json
// @Success 200 {array} service.Notification
// @Failure 500
// @Router /notifications [get]
func (r *NotificationRouter) GetAll(c *fiber.Ctx) error {
userID := GetUserID(c)
res, err := r.notificationSvc.GetAll(userID)
if err != nil {
return err
}
return c.JSON(res)
}

View File

@@ -0,0 +1,243 @@
package router
import (
"net/http"
"strconv"
"voltaserve/errorpkg"
"voltaserve/service"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type OrganizationRouter struct {
orgSvc *service.OrganizationService
}
func NewOrganizationRouter() *OrganizationRouter {
return &OrganizationRouter{
orgSvc: service.NewOrganizationService(),
}
}
func (r *OrganizationRouter) AppendRoutes(g fiber.Router) {
g.Get("/", r.List)
g.Post("/", r.Create)
g.Get("/:id", r.GetByID)
g.Delete("/:id", r.Delete)
g.Post("/:id/update_name", r.UpdateName)
g.Post("/:id/leave", r.Leave)
g.Post("/:id/remove_member", r.RemoveMember)
}
// Create godoc
//
// @Summary Create
// @Description Create
// @Tags Organizations
// @Id organizations_create
// @Accept json
// @Produce json
// @Param body body service.OrganizationCreateOptions true "Body"
// @Success 200 {object} service.Organization
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /organizations [post]
func (r *OrganizationRouter) Create(c *fiber.Ctx) error {
userID := GetUserID(c)
req := new(service.OrganizationCreateOptions)
if err := c.BodyParser(req); err != nil {
return err
}
if err := validator.New().Struct(req); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
res, err := r.orgSvc.Create(service.OrganizationCreateOptions{
Name: req.Name,
Image: req.Image,
}, userID)
if err != nil {
return err
}
return c.Status(http.StatusCreated).JSON(res)
}
// GetByID godoc
//
// @Summary Get by ID
// @Description Get by ID
// @Tags Organizations
// @Id organizations_get_by_id
// @Produce json
// @Param id path string true "ID"
// @Success 200 {object} service.Organization
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /organizations/{id} [get]
func (r *OrganizationRouter) GetByID(c *fiber.Ctx) error {
userID := GetUserID(c)
res, err := r.orgSvc.Find(c.Params("id"), userID)
if err != nil {
return err
}
return c.JSON(res)
}
// Delete godoc
//
// @Summary Delete
// @Description Delete
// @Tags Organizations
// @Id organizations_delete
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Success 200
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /organizations/{id} [delete]
func (r *OrganizationRouter) Delete(c *fiber.Ctx) error {
userID := GetUserID(c)
if err := r.orgSvc.Delete(c.Params("id"), userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}
// UpdateName godoc
//
// @Summary Update name
// @Description Update name
// @Tags Organizations
// @Id organizations_update_name
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Param body body service.OrganizationUpdateNameOptions true "Body"
// @Success 200 {object} service.Organization
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /organizations/{id}/update_name [post]
func (r *OrganizationRouter) UpdateName(c *fiber.Ctx) error {
userID := GetUserID(c)
req := new(service.OrganizationUpdateNameOptions)
if err := c.BodyParser(req); err != nil {
return err
}
if err := validator.New().Struct(req); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
res, err := r.orgSvc.UpdateName(c.Params("id"), req.Name, userID)
if err != nil {
return err
}
return c.JSON(res)
}
// List godoc
//
// @Summary List
// @Description List
// @Tags Organizations
// @Id organizations_list
// @Produce json
// @Param query query string false "Query"
// @Param page query string false "Page"
// @Param size query string false "Size"
// @Param sort_by query string false "Sort By"
// @Param sort_order query string false "Sort Order"
// @Success 200 {object} service.WorkspaceList
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /organizations [get]
func (r *OrganizationRouter) List(c *fiber.Ctx) error {
var err error
var page int64
if c.Query("page") == "" {
page = 1
} else {
page, err = strconv.ParseInt(c.Query("page"), 10, 32)
if err != nil {
page = 1
}
}
var size int64
if c.Query("size") == "" {
size = OrganizationDefaultPageSize
} else {
size, err = strconv.ParseInt(c.Query("size"), 10, 32)
if err != nil {
return err
}
}
sortBy := c.Query("sort_by")
if !IsValidSortBy(sortBy) {
return errorpkg.NewInvalidQueryParamError("sort_by")
}
sortOrder := c.Query("sort_order")
if !IsValidSortOrder(sortOrder) {
return errorpkg.NewInvalidQueryParamError("sort_order")
}
res, err := r.orgSvc.List(service.OrganizationListOptions{
Query: c.Query("query"),
Page: uint(page),
Size: uint(size),
SortBy: sortBy,
SortOrder: sortOrder,
}, GetUserID(c))
if err != nil {
return err
}
return c.JSON(res)
}
// Leave godoc
//
// @Summary Leave
// @Description Leave
// @Tags Organizations
// @Id organizations_leave\
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /organizations/{id}/leave [post]
func (r *OrganizationRouter) Leave(c *fiber.Ctx) error {
userID := GetUserID(c)
if err := r.orgSvc.RemoveMember(c.Params("id"), userID, userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}
// RemoveMember godoc
//
// @Summary Remove member
// @Description Remove member
// @Tags Organizations
// @Id organizations_remove_member
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Param body body service.OrganizationRemoveMemberOptions true "Body"
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /organizations/{id}/remove_member [post]
func (r *OrganizationRouter) RemoveMember(c *fiber.Ctx) error {
userID := GetUserID(c)
req := new(service.OrganizationRemoveMemberOptions)
if err := c.BodyParser(req); err != nil {
return err
}
if err := validator.New().Struct(req); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
if err := r.orgSvc.RemoveMember(c.Params("id"), req.UserID, userID); err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}

View File

@@ -0,0 +1,88 @@
package router
import (
"voltaserve/errorpkg"
"voltaserve/service"
"github.com/gofiber/fiber/v2"
)
type StorageRouter struct {
storageSvc *service.StorageService
}
func NewStorageRouter() *StorageRouter {
return &StorageRouter{
storageSvc: service.NewStorageService(),
}
}
func (r *StorageRouter) AppendRoutes(g fiber.Router) {
g.Get("/get_account_usage", r.GetAccountUsage)
g.Get("/get_workspace_usage", r.GetWorkspaceUsage)
g.Get("/get_file_usage", r.GetFileUsage)
}
// GetAccountUsage godoc
//
// @Summary Get account usage
// @Description Get account usage
// @Tags Storage
// @Id storage_get_account_usage
// @Produce json
// @Success 200 {object} service.StorageUsage
// @Failure 500
// @Router /storage/get_account_usage [get]
func (r *StorageRouter) GetAccountUsage(c *fiber.Ctx) error {
res, err := r.storageSvc.GetAccountUsage(GetUserID(c))
if err != nil {
return err
}
return c.JSON(res)
}
// GetWorkspaceUsage godoc
//
// @Summary Get workspace usage
// @Description Get workspace usage
// @Tags Storage
// @Id storage_get_workspace_usage
// @Produce json
// @Param id query string true "Workspace ID"
// @Success 200 {object} service.StorageUsage
// @Failure 500
// @Router /storage/get_workspace_usage [get]
func (r *StorageRouter) GetWorkspaceUsage(c *fiber.Ctx) error {
id := c.Query("id")
if id == "" {
return errorpkg.NewMissingQueryParamError("id")
}
res, err := r.storageSvc.GetWorkspaceUsage(id, GetUserID(c))
if err != nil {
return err
}
return c.JSON(res)
}
// GetFileUsage godoc
//
// @Summary Get file usage
// @Description Get file usage
// @Tags Storage
// @Id storage_get_file_usage
// @Produce json
// @Param id query string true "File ID"
// @Success 200 {object} service.StorageUsage
// @Failure 500
// @Router /storage/get_file_usage [get]
func (r *StorageRouter) GetFileUsage(c *fiber.Ctx) error {
id := c.Query("id")
if id == "" {
return errorpkg.NewMissingQueryParamError("id")
}
res, err := r.storageSvc.GetFileUsage(id, GetUserID(c))
if err != nil {
return err
}
return c.JSON(res)
}

View File

@@ -0,0 +1,12 @@
package router
import (
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
func GetUserID(c *fiber.Ctx) string {
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
return claims["sub"].(string)
}

View File

@@ -0,0 +1,93 @@
package router
import (
"strconv"
"voltaserve/errorpkg"
"voltaserve/service"
"github.com/gofiber/fiber/v2"
)
type UserRouter struct {
userSvc *service.UserService
}
func NewUserRouter() *UserRouter {
return &UserRouter{
userSvc: service.NewUserService(),
}
}
func (r *UserRouter) AppendRoutes(g fiber.Router) {
g.Get("/", r.List)
}
// List godoc
//
// @Summary List
// @Description List
// @Tags Users
// @Id users_list
// @Produce json
// @Param query query string false "Query"
// @Param organization_id query string false "Organization ID"
// @Param group query string false "Group ID"
// @Param page query string false "Page"
// @Param size query string false "Size"
// @Param sort_by query string false "Sort By"
// @Param sort_order query string false "Sort Order"
// @Success 200 {object} service.UserList
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /users [get]
func (r *UserRouter) List(c *fiber.Ctx) error {
var err error
var page int64
if c.Query("page") == "" {
page = 1
} else {
page, err = strconv.ParseInt(c.Query("page"), 10, 32)
if err != nil {
page = 1
}
}
var size int64
if c.Query("size") == "" {
size = UserDefaultPageSize
} else {
size, err = strconv.ParseInt(c.Query("size"), 10, 32)
if err != nil {
return err
}
}
sortBy := c.Query("sort_by")
if !IsValidSortBy(sortBy) {
return errorpkg.NewInvalidQueryParamError("sort_by")
}
sortOrder := c.Query("sort_order")
if !IsValidSortOrder(sortOrder) {
return errorpkg.NewInvalidQueryParamError("sort_order")
}
userID := GetUserID(c)
var nonGroupMembersOnly bool
if c.Query("non_group_members_only") != "" {
nonGroupMembersOnly, err = strconv.ParseBool(c.Query("non_group_members_only"))
if err != nil {
return err
}
}
res, err := r.userSvc.List(service.UserListOptions{
Query: c.Query("query"),
OrganizationID: c.Query("organization_id"),
GroupID: c.Query("group_id"),
NonGroupMembersOnly: nonGroupMembersOnly,
SortBy: sortBy,
SortOrder: sortOrder,
Page: uint(page),
Size: uint(size),
}, userID)
if err != nil {
return err
}
return c.JSON(res)
}

View File

@@ -0,0 +1,11 @@
package router
import "voltaserve/service"
func IsValidSortBy(value string) bool {
return value == "" || value == service.SortByName || value == service.SortByKind || value == service.SortBySize || value == service.SortByDateCreated || value == service.SortByDateModified || value == service.SortByEmail || value == service.SortByFullName
}
func IsValidSortOrder(value string) bool {
return value == "" || value == service.SortOrderAsc || value == service.SortOrderDesc
}

View File

@@ -0,0 +1,208 @@
package router
import (
"net/http"
"strconv"
"voltaserve/errorpkg"
"voltaserve/service"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type WorkspaceRouter struct {
workspaceSvc *service.WorkspaceService
}
func NewWorkspaceRouter() *WorkspaceRouter {
return &WorkspaceRouter{
workspaceSvc: service.NewWorkspaceService(),
}
}
func (r *WorkspaceRouter) AppendRoutes(g fiber.Router) {
g.Get("/", r.List)
g.Post("/", r.Create)
g.Get("/:id", r.GetByID)
g.Delete("/:id", r.Delete)
g.Post("/:id/update_name", r.UpdateName)
g.Post("/:id/update_storage_capacity", r.UpdateStorageCapacity)
}
// Create godoc
//
// @Summary Create
// @Description Create
// @Tags Workspaces
// @Id workspaces_create
// @Accept json
// @Produce json
// @Param body body service.WorkspaceCreateOptions true "Body"
// @Success 200 {object} service.Workspace
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /workspaces [post]
func (r *WorkspaceRouter) Create(c *fiber.Ctx) error {
userID := GetUserID(c)
opts := new(service.WorkspaceCreateOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
if err := validator.New().Struct(opts); err != nil {
return errorpkg.NewRequestBodyValidationError(err)
}
res, err := r.workspaceSvc.Create(*opts, userID)
if err != nil {
return err
}
return c.Status(http.StatusCreated).JSON(res)
}
// GetByID godoc
//
// @Summary Get by ID
// @Description Get by ID
// @Tags Workspaces
// @Id workspaces_get_by_id
// @Produce json
// @Param id path string true "ID"
// @Success 200 {object} service.Workspace
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /workspaces/{id} [get]
func (r *WorkspaceRouter) GetByID(c *fiber.Ctx) error {
res, err := r.workspaceSvc.Find(c.Params("id"), GetUserID(c))
if err != nil {
return err
}
return c.JSON(res)
}
// List godoc
//
// @Summary List
// @Description List
// @Tags Workspaces
// @Id workspaces_list
// @Produce json
// @Param query query string false "Query"
// @Param page query string false "Page"
// @Param size query string false "Size"
// @Param sort_by query string false "Sort By"
// @Param sort_order query string false "Sort Order"
// @Success 200 {object} service.WorkspaceList
// @Failure 404 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /workspaces [get]
func (r *WorkspaceRouter) List(c *fiber.Ctx) error {
var err error
var page int64
if c.Query("page") == "" {
page = 1
} else {
page, err = strconv.ParseInt(c.Query("page"), 10, 32)
if err != nil {
page = 1
}
}
var size int64
if c.Query("size") == "" {
size = WorkspaceDefaultPageSize
} else {
size, err = strconv.ParseInt(c.Query("size"), 10, 32)
if err != nil {
return err
}
}
sortBy := c.Query("sort_by")
if !IsValidSortBy(sortBy) {
return errorpkg.NewInvalidQueryParamError("sort_by")
}
sortOrder := c.Query("sort_order")
if !IsValidSortOrder(sortOrder) {
return errorpkg.NewInvalidQueryParamError("sort_order")
}
res, err := r.workspaceSvc.List(service.WorkspaceListOptions{
Query: c.Query("query"),
Page: uint(page),
Size: uint(size),
SortBy: sortBy,
SortOrder: sortOrder,
}, GetUserID(c))
if err != nil {
return err
}
return c.JSON(res)
}
// UpdateName godoc
//
// @Summary Update name
// @Description Update name
// @Tags Workspaces
// @Id workspaces_update_name
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Param body body service.WorkspaceUpdateNameOptions true "Body"
// @Success 200 {object} service.Workspace
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /workspaces/{id}/update_name [post]
func (r *WorkspaceRouter) UpdateName(c *fiber.Ctx) error {
opts := new(service.WorkspaceUpdateNameOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
res, err := r.workspaceSvc.UpdateName(c.Params("id"), opts.Name, GetUserID(c))
if err != nil {
return err
}
return c.JSON(res)
}
// UpdateStorageCapacity godoc
//
// @Summary Update storage capacity
// @Description Update storage capacity
// @Tags Workspaces
// @Id workspaces_update_storage_capacity
// @Accept json
// @Produce json
// @Param id path string true "Id"
// @Param body body service.WorkspaceUpdateStorageCapacityOptions true "Body"
// @Success 200 {object} service.Workspace
// @Failure 400 {object} errorpkg.ErrorResponse
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /workspaces/{id}/update_storage_capacity [post]
func (r *WorkspaceRouter) UpdateStorageCapacity(c *fiber.Ctx) error {
opts := new(service.WorkspaceUpdateStorageCapacityOptions)
if err := c.BodyParser(opts); err != nil {
return err
}
res, err := r.workspaceSvc.UpdateStorageCapacity(c.Params("id"), opts.StorageCapacity, GetUserID(c))
if err != nil {
return err
}
return c.JSON(res)
}
// Delete godoc
//
// @Summary Delete
// @Description Delete
// @Tags Workspaces
// @Id workspaces_delete
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Success 200
// @Failure 500 {object} errorpkg.ErrorResponse
// @Router /workspaces/{id} [delete]
func (r *WorkspaceRouter) Delete(c *fiber.Ctx) error {
err := r.workspaceSvc.Delete(c.Params("id"), GetUserID(c))
if err != nil {
return err
}
return c.SendStatus(http.StatusNoContent)
}

View File

@@ -0,0 +1,103 @@
package search
import (
"encoding/json"
"voltaserve/infra"
"voltaserve/model"
"voltaserve/repo"
)
type FileSearch struct {
search *infra.SearchManager
index string
s3 *infra.S3Manager
}
func NewFileSearch() *FileSearch {
return &FileSearch{
index: infra.FileSearchIndex,
search: infra.NewSearchManager(),
s3: infra.NewS3Manager(),
}
}
func (s *FileSearch) Index(files []model.File) (err error) {
if len(files) == 0 {
return nil
}
if err = s.populateTextField(files); err != nil {
return err
}
var res []infra.SearchModel
for _, f := range files {
res = append(res, f)
}
if err := s.search.Index(s.index, res); err != nil {
return err
}
return nil
}
func (s *FileSearch) Update(files []model.File) (err error) {
if len(files) == 0 {
return nil
}
if err = s.populateTextField(files); err != nil {
return err
}
var res []infra.SearchModel
for _, f := range files {
res = append(res, f)
}
if err := s.search.Update(s.index, res); err != nil {
return err
}
return nil
}
func (s *FileSearch) Delete(ids []string) error {
if len(ids) == 0 {
return nil
}
if err := s.search.Delete(s.index, ids); err != nil {
return err
}
return nil
}
func (s *FileSearch) Query(query string) ([]model.File, error) {
hits, err := s.search.Query(s.index, query)
if err != nil {
return nil, err
}
var res []model.File
for _, v := range hits {
var b []byte
b, err = json.Marshal(v)
if err != nil {
return nil, err
}
file := repo.NewFile()
if err = json.Unmarshal(b, &file); err != nil {
return nil, err
}
res = append(res, file)
}
return res, nil
}
func (s *FileSearch) populateTextField(files []model.File) error {
for _, f := range files {
if f.GetSnapshots() != nil &&
len(f.GetSnapshots()) > 0 &&
f.GetSnapshots()[0].HasText() {
var text string
text, err := s.s3.GetText(f.GetSnapshots()[0].GetText().Key, f.GetSnapshots()[0].GetText().Bucket)
if err != nil {
return err
}
f.SetText(&text)
}
}
return nil
}

View File

@@ -0,0 +1,81 @@
package search
import (
"encoding/json"
"voltaserve/infra"
"voltaserve/model"
"voltaserve/repo"
)
type GroupSearch struct {
index string
search *infra.SearchManager
groupRepo repo.GroupRepo
}
func NewGroupSearch() *GroupSearch {
return &GroupSearch{
index: infra.GroupSearchIndex,
search: infra.NewSearchManager(),
groupRepo: repo.NewGroupRepo(),
}
}
func (s *GroupSearch) Index(groups []model.Group) error {
if len(groups) == 0 {
return nil
}
var res []infra.SearchModel
for _, g := range groups {
res = append(res, g)
}
if err := s.search.Index(s.index, res); err != nil {
return err
}
return nil
}
func (s *GroupSearch) Update(groups []model.Group) error {
if len(groups) == 0 {
return nil
}
var res []infra.SearchModel
for _, g := range groups {
res = append(res, g)
}
if err := s.search.Update(s.index, res); err != nil {
return err
}
return nil
}
func (s *GroupSearch) Delete(ids []string) error {
if len(ids) == 0 {
return nil
}
if err := s.search.Delete(s.index, ids); err != nil {
return err
}
return nil
}
func (s *GroupSearch) Query(query string) ([]model.Group, error) {
hits, err := s.search.Query(s.index, query)
if err != nil {
return nil, err
}
var res []model.Group
for _, v := range hits {
var b []byte
b, err = json.Marshal(v)
if err != nil {
return nil, err
}
group := repo.NewGroup()
if err = json.Unmarshal(b, &group); err != nil {
return nil, err
}
res = append(res, group)
}
return res, nil
}

View File

@@ -0,0 +1,81 @@
package search
import (
"encoding/json"
"voltaserve/infra"
"voltaserve/model"
"voltaserve/repo"
)
type OrganizationSearch struct {
index string
search *infra.SearchManager
orgRepo repo.OrganizationRepo
}
func NewOrganizationSearch() *OrganizationSearch {
return &OrganizationSearch{
index: infra.OrganizationSearchIndex,
search: infra.NewSearchManager(),
orgRepo: repo.NewOrganizationRepo(),
}
}
func (s *OrganizationSearch) Index(orgs []model.Organization) error {
if len(orgs) == 0 {
return nil
}
var res []infra.SearchModel
for _, o := range orgs {
res = append(res, o)
}
if err := s.search.Index(s.index, res); err != nil {
return err
}
return nil
}
func (s *OrganizationSearch) Update(orgs []model.Organization) error {
if len(orgs) == 0 {
return nil
}
var res []infra.SearchModel
for _, o := range orgs {
res = append(res, o)
}
if err := s.search.Update(s.index, res); err != nil {
return err
}
return nil
}
func (s *OrganizationSearch) Delete(ids []string) error {
if len(ids) == 0 {
return nil
}
if err := s.search.Delete(s.index, ids); err != nil {
return err
}
return nil
}
func (s *OrganizationSearch) Query(query string) ([]model.Organization, error) {
hits, err := s.search.Query(s.index, query)
if err != nil {
return nil, err
}
var res []model.Organization
for _, v := range hits {
var b []byte
b, err = json.Marshal(v)
if err != nil {
return nil, err
}
org := repo.NewOrganization()
if err = json.Unmarshal(b, &org); err != nil {
return nil, err
}
res = append(res, org)
}
return res, nil
}

View File

@@ -0,0 +1,40 @@
package search
import (
"encoding/json"
"voltaserve/infra"
"voltaserve/model"
"voltaserve/repo"
)
type UserSearch struct {
index string
search *infra.SearchManager
}
func NewUserSearch() *UserSearch {
return &UserSearch{
index: infra.UserSearchIndex,
search: infra.NewSearchManager(),
}
}
func (s *UserSearch) Query(query string) ([]model.User, error) {
hits, err := s.search.Query(s.index, query)
if err != nil {
return nil, err
}
res := []model.User{}
for _, v := range hits {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
user := repo.NewUser()
if err := json.Unmarshal(b, &user); err != nil {
return nil, err
}
res = append(res, user)
}
return res, nil
}

View File

@@ -0,0 +1,81 @@
package search
import (
"encoding/json"
"voltaserve/infra"
"voltaserve/model"
"voltaserve/repo"
)
type WorkspaceSearch struct {
index string
search *infra.SearchManager
workspaceRepo repo.WorkspaceRepo
}
func NewWorkspaceSearch() *WorkspaceSearch {
return &WorkspaceSearch{
index: infra.WorkspaceSearchIndex,
search: infra.NewSearchManager(),
workspaceRepo: repo.NewWorkspaceRepo(),
}
}
func (s *WorkspaceSearch) Index(workspaces []model.Workspace) error {
if len(workspaces) == 0 {
return nil
}
var res []infra.SearchModel
for _, w := range workspaces {
res = append(res, w)
}
if err := s.search.Index(s.index, res); err != nil {
return err
}
return nil
}
func (s *WorkspaceSearch) Update(workspaces []model.Workspace) error {
if len(workspaces) == 0 {
return nil
}
var res []infra.SearchModel
for _, w := range workspaces {
res = append(res, w)
}
if err := s.search.Update(s.index, res); err != nil {
return err
}
return nil
}
func (s *WorkspaceSearch) Delete(ids []string) error {
if len(ids) == 0 {
return nil
}
if err := s.search.Delete(s.index, ids); err != nil {
return err
}
return nil
}
func (s *WorkspaceSearch) Query(query string) ([]model.Workspace, error) {
hits, err := s.search.Query(s.index, query)
if err != nil {
return nil, err
}
var res []model.Workspace
for _, v := range hits {
var b []byte
b, err = json.Marshal(v)
if err != nil {
return nil, err
}
workspace := repo.NewWorkspace()
if err = json.Unmarshal(b, &workspace); err != nil {
return nil, err
}
res = append(res, workspace)
}
return res, nil
}

View File

@@ -0,0 +1,12 @@
package service
const SortByEmail = "email"
const SortByFullName = "full_name"
const SortByName = "name"
const SortByKind = "kind"
const SortBySize = "size"
const SortByDateCreated = "date_created"
const SortByDateModified = "date_modified"
const SortOrderAsc = "asc"
const SortOrderDesc = "desc"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,520 @@
package service
import (
"sort"
"time"
"voltaserve/cache"
"voltaserve/config"
"voltaserve/guard"
"voltaserve/helper"
"voltaserve/model"
"voltaserve/repo"
"voltaserve/search"
)
type Group struct {
ID string `json:"id"`
Name string `json:"name"`
Image *string `json:"image,omitempty"`
Organization Organization `json:"organization"`
Permission string `json:"permission"`
CreateTime string `json:"createTime,omitempty"`
UpdateTime *string `json:"updateTime"`
}
type GroupList struct {
Data []*Group `json:"data"`
TotalPages uint `json:"totalPages"`
TotalElements uint `json:"totalElements"`
Page uint `json:"page"`
Size uint `json:"size"`
}
type GroupCreateOptions struct {
Name string `json:"name" validate:"required,max=255"`
Image *string `json:"image"`
OrganizationID string `json:"organizationId" validate:"required"`
}
type GroupListOptions struct {
Query string
OrganizationID string
Page uint
Size uint
SortBy string
SortOrder string
}
type GroupUpdateNameOptions struct {
Name string `json:"name" validate:"required,max=255"`
}
type GroupUpdateImageOptions struct {
Image string `json:"image" validate:"required,base64"`
}
type GroupAddMemberOptions struct {
UserID string `json:"userId" validate:"required"`
}
type GroupRemoveMemberOptions struct {
UserID string `json:"userId" validate:"required"`
}
type GroupService struct {
groupRepo repo.GroupRepo
groupGuard *guard.GroupGuard
groupSearch *search.GroupSearch
groupMapper *groupMapper
groupCache *cache.GroupCache
userRepo repo.UserRepo
userSearch *search.UserSearch
userMapper *userMapper
workspaceRepo repo.WorkspaceRepo
workspaceCache *cache.WorkspaceCache
fileRepo repo.FileRepo
fileCache *cache.FileCache
fileGuard *guard.FileGuard
orgRepo repo.OrganizationRepo
orgCache *cache.OrganizationCache
orgGuard *guard.OrganizationGuard
config config.Config
}
func NewGroupService() *GroupService {
return &GroupService{
groupRepo: repo.NewGroupRepo(),
groupGuard: guard.NewGroupGuard(),
groupCache: cache.NewGroupCache(),
groupSearch: search.NewGroupSearch(),
groupMapper: newGroupMapper(),
userRepo: repo.NewUserRepo(),
userSearch: search.NewUserSearch(),
userMapper: newUserMapper(),
workspaceRepo: repo.NewWorkspaceRepo(),
workspaceCache: cache.NewWorkspaceCache(),
fileRepo: repo.NewFileRepo(),
fileCache: cache.NewFileCache(),
orgRepo: repo.NewOrganizationRepo(),
orgGuard: guard.NewOrganizationGuard(),
orgCache: cache.NewOrganizationCache(),
fileGuard: guard.NewFileGuard(),
config: config.GetConfig(),
}
}
func (svc *GroupService) Create(opts GroupCreateOptions, userID string) (*Group, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
org, err := svc.orgCache.Get(opts.OrganizationID)
if err != nil {
return nil, err
}
if err := svc.orgGuard.Authorize(user, org, model.PermissionEditor); err != nil {
return nil, err
}
group, err := svc.groupRepo.Insert(repo.GroupInsertOptions{
ID: helper.NewID(),
Name: opts.Name,
OrganizationID: opts.OrganizationID,
OwnerID: userID,
})
if err != nil {
return nil, err
}
if err := svc.groupRepo.GrantUserPermission(group.GetID(), userID, model.PermissionOwner); err != nil {
return nil, err
}
group, err = svc.groupRepo.Find(group.GetID())
if err != nil {
return nil, err
}
if err := svc.groupSearch.Index([]model.Group{group}); err != nil {
return nil, err
}
if err := svc.groupCache.Set(group); err != nil {
return nil, err
}
res, err := svc.groupMapper.mapOne(group, userID)
if err != nil {
return nil, err
}
return res, nil
}
func (svc *GroupService) Find(id string, userID string) (*Group, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
group, err := svc.groupCache.Get(id)
if err != nil {
return nil, err
}
if err := svc.groupGuard.Authorize(user, group, model.PermissionViewer); err != nil {
return nil, err
}
res, err := svc.groupMapper.mapOne(group, userID)
if err != nil {
return nil, err
}
return res, nil
}
func (svc *GroupService) List(opts GroupListOptions, userID string) (*GroupList, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
var authorized []model.Group
if opts.Query == "" {
if opts.OrganizationID == "" {
ids, err := svc.groupRepo.GetIDs()
if err != nil {
return nil, err
}
authorized, err = svc.doAuthorizationByIDs(ids, user)
if err != nil {
return nil, err
}
} else {
groups, err := svc.orgRepo.GetGroups(opts.OrganizationID)
if err != nil {
return nil, err
}
authorized, err = svc.doAuthorization(groups, user)
if err != nil {
return nil, err
}
}
} else {
groups, err := svc.groupSearch.Query(opts.Query)
if err != nil {
return nil, err
}
var filtered []model.Group
if opts.OrganizationID == "" {
filtered = groups
} else {
for _, g := range groups {
if g.GetOrganizationID() == opts.OrganizationID {
filtered = append(filtered, g)
}
}
}
authorized, err = svc.doAuthorization(filtered, user)
if err != nil {
return nil, err
}
}
if opts.SortBy == "" {
opts.SortBy = SortByDateCreated
}
if opts.SortOrder == "" {
opts.SortOrder = SortOrderAsc
}
sorted := svc.doSorting(authorized, opts.SortBy, opts.SortOrder)
paged, totalElements, totalPages := svc.doPagination(sorted, opts.Page, opts.Size)
mapped, err := svc.groupMapper.mapMany(paged, userID)
if err != nil {
return nil, err
}
return &GroupList{
Data: mapped,
TotalPages: totalPages,
TotalElements: totalElements,
Page: opts.Page,
Size: uint(len(mapped)),
}, nil
}
func (svc *GroupService) UpdateName(id string, name string, userID string) (*Group, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
group, err := svc.groupCache.Get(id)
if err != nil {
return nil, err
}
if err := svc.groupGuard.Authorize(user, group, model.PermissionEditor); err != nil {
return nil, err
}
group.SetName(name)
if err := svc.groupRepo.Save(group); err != nil {
return nil, err
}
if err := svc.groupSearch.Update([]model.Group{group}); err != nil {
return nil, err
}
err = svc.groupCache.Set(group)
if err != nil {
return nil, err
}
res, err := svc.groupMapper.mapOne(group, userID)
if err != nil {
return nil, err
}
return res, nil
}
func (svc *GroupService) Delete(id string, userID string) error {
user, err := svc.userRepo.Find(userID)
if err != nil {
return err
}
group, err := svc.groupCache.Get(id)
if err != nil {
return nil
}
if err := svc.groupGuard.Authorize(user, group, model.PermissionOwner); err != nil {
return err
}
if err := svc.groupRepo.Delete(id); err != nil {
return err
}
if err := svc.groupSearch.Delete([]string{group.GetID()}); err != nil {
return err
}
if err := svc.refreshCacheForOrganization(group.GetOrganizationID()); err != nil {
return err
}
return nil
}
func (svc *GroupService) AddMember(id string, memberID string, userID string) error {
user, err := svc.userRepo.Find(userID)
if err != nil {
return err
}
group, err := svc.groupCache.Get(id)
if err != nil {
return nil
}
if err := svc.groupGuard.Authorize(user, group, model.PermissionOwner); err != nil {
return err
}
if _, err := svc.userRepo.Find(memberID); err != nil {
return err
}
if err := svc.groupRepo.AddUser(id, memberID); err != nil {
return err
}
if err := svc.groupRepo.GrantUserPermission(group.GetID(), memberID, model.PermissionViewer); err != nil {
return err
}
if _, err := svc.groupCache.Refresh(group.GetID()); err != nil {
return err
}
if err := svc.refreshCacheForOrganization(group.GetOrganizationID()); err != nil {
return err
}
return nil
}
func (svc *GroupService) RemoveMember(id string, memberID string, userID string) error {
user, err := svc.userRepo.Find(userID)
if err != nil {
return err
}
group, err := svc.groupCache.Get(id)
if err != nil {
return nil
}
if err := svc.groupGuard.Authorize(user, group, model.PermissionOwner); err != nil {
return err
}
if err := svc.RemoveMemberUnauthorized(id, memberID); err != nil {
return err
}
return nil
}
func (svc *GroupService) RemoveMemberUnauthorized(id string, memberID string) error {
group, err := svc.groupCache.Get(id)
if err != nil {
return nil
}
if _, err := svc.userRepo.Find(memberID); err != nil {
return err
}
if err := svc.groupRepo.RemoveMember(id, memberID); err != nil {
return err
}
if err := svc.groupRepo.RevokeUserPermission(id, memberID); err != nil {
return err
}
if _, err := svc.groupCache.Refresh(group.GetID()); err != nil {
return err
}
if err := svc.refreshCacheForOrganization(group.GetOrganizationID()); err != nil {
return err
}
return nil
}
func (svc *GroupService) refreshCacheForOrganization(orgID string) error {
workspaceIDs, err := svc.workspaceRepo.GetIDsByOrganization(orgID)
if err != nil {
return err
}
for _, workspaceID := range workspaceIDs {
if _, err := svc.workspaceCache.Refresh(workspaceID); err != nil {
return err
}
filesIDs, err := svc.fileRepo.GetIDsByWorkspace(workspaceID)
if err != nil {
return err
}
for _, id := range filesIDs {
if _, err := svc.fileCache.Refresh(id); err != nil {
return err
}
}
}
return nil
}
func (svc *GroupService) doAuthorization(data []model.Group, user model.User) ([]model.Group, error) {
var res []model.Group
for _, g := range data {
if svc.groupGuard.IsAuthorized(user, g, model.PermissionViewer) {
res = append(res, g)
}
}
return res, nil
}
func (svc *GroupService) doAuthorizationByIDs(ids []string, user model.User) ([]model.Group, error) {
var res []model.Group
for _, id := range ids {
var o model.Group
o, err := svc.groupCache.Get(id)
if err != nil {
return nil, err
}
if svc.groupGuard.IsAuthorized(user, o, model.PermissionViewer) {
res = append(res, o)
}
}
return res, nil
}
func (svc *GroupService) doSorting(data []model.Group, sortBy string, sortOrder string) []model.Group {
if sortBy == SortByName {
sort.Slice(data, func(i, j int) bool {
if sortOrder == SortOrderDesc {
return data[i].GetName() > data[j].GetName()
} else {
return data[i].GetName() < data[j].GetName()
}
})
return data
} else if sortBy == SortByDateCreated {
sort.Slice(data, func(i, j int) bool {
a, _ := time.Parse(time.RFC3339, data[i].GetCreateTime())
b, _ := time.Parse(time.RFC3339, data[j].GetCreateTime())
if sortOrder == SortOrderDesc {
return a.UnixMilli() > b.UnixMilli()
} else {
return a.UnixMilli() < b.UnixMilli()
}
})
return data
} else if sortBy == SortByDateModified {
sort.Slice(data, func(i, j int) bool {
if data[i].GetUpdateTime() != nil && data[j].GetUpdateTime() != nil {
a, _ := time.Parse(time.RFC3339, *data[i].GetUpdateTime())
b, _ := time.Parse(time.RFC3339, *data[j].GetUpdateTime())
if sortOrder == SortOrderDesc {
return a.UnixMilli() > b.UnixMilli()
} else {
return a.UnixMilli() < b.UnixMilli()
}
} else {
return false
}
})
return data
}
return data
}
func (svc *GroupService) doPagination(data []model.Group, page, size uint) ([]model.Group, uint, uint) {
totalElements := uint(len(data))
totalPages := (totalElements + size - 1) / size
if page > totalPages {
return nil, totalElements, totalPages
}
startIndex := (page - 1) * size
endIndex := startIndex + size
if endIndex > totalElements {
endIndex = totalElements
}
pageData := data[startIndex:endIndex]
return pageData, totalElements, totalPages
}
type groupMapper struct {
orgCache *cache.OrganizationCache
orgMapper *organizationMapper
groupCache *cache.GroupCache
}
func newGroupMapper() *groupMapper {
return &groupMapper{
orgCache: cache.NewOrganizationCache(),
orgMapper: newOrganizationMapper(),
groupCache: cache.NewGroupCache(),
}
}
func (mp *groupMapper) mapOne(m model.Group, userID string) (*Group, error) {
org, err := mp.orgCache.Get(m.GetOrganizationID())
if err != nil {
return nil, err
}
v, err := mp.orgMapper.mapOne(org, userID)
if err != nil {
return nil, err
}
res := &Group{
ID: m.GetID(),
Name: m.GetName(),
Organization: *v,
CreateTime: m.GetCreateTime(),
UpdateTime: m.GetUpdateTime(),
}
res.Permission = ""
for _, p := range m.GetUserPermissions() {
if p.GetUserID() == userID && model.GetPermissionWeight(p.GetValue()) > model.GetPermissionWeight(res.Permission) {
res.Permission = p.GetValue()
}
}
for _, p := range m.GetGroupPermissions() {
g, err := mp.groupCache.Get(p.GetGroupID())
if err != nil {
return nil, err
}
for _, u := range g.GetUsers() {
if u == userID && model.GetPermissionWeight(p.GetValue()) > model.GetPermissionWeight(res.Permission) {
res.Permission = p.GetValue()
}
}
}
return res, nil
}
func (mp *groupMapper) mapMany(groups []model.Group, userID string) ([]*Group, error) {
res := []*Group{}
for _, g := range groups {
v, err := mp.mapOne(g, userID)
if err != nil {
return nil, err
}
res = append(res, v)
}
return res, nil
}

View File

@@ -0,0 +1,428 @@
package service
import (
"sort"
"strings"
"time"
"voltaserve/cache"
"voltaserve/config"
"voltaserve/errorpkg"
"voltaserve/guard"
"voltaserve/infra"
"voltaserve/model"
"voltaserve/repo"
)
type Invitation struct {
ID string `json:"id"`
Owner *User `json:"owner,omitempty"`
Email string `json:"email"`
Organization *Organization `json:"organization,omitempty"`
Status string `json:"status"`
CreateTime string `json:"createTime"`
UpdateTime *string `json:"updateTime"`
}
type InvitationList struct {
Data []*Invitation `json:"data"`
TotalPages uint `json:"totalPages"`
TotalElements uint `json:"totalElements"`
Page uint `json:"page"`
Size uint `json:"size"`
}
type InvitationCreateOptions struct {
OrganizationID string `json:"organizationId" validate:"required"`
Emails []string `json:"emails" validate:"required,dive,email"`
}
type InvitationListOptions struct {
Page uint
Size uint
SortBy string
SortOrder string
}
type InvitationService struct {
orgRepo repo.OrganizationRepo
orgMapper *organizationMapper
invitationRepo repo.InvitationRepo
invitationMapper *invitationMapper
orgCache *cache.OrganizationCache
orgGuard *guard.OrganizationGuard
userRepo repo.UserRepo
mailTmpl *infra.MailTemplate
config config.Config
}
func NewInvitationService() *InvitationService {
return &InvitationService{
orgRepo: repo.NewOrganizationRepo(),
orgCache: cache.NewOrganizationCache(),
orgGuard: guard.NewOrganizationGuard(),
invitationRepo: repo.NewInvitationRepo(),
invitationMapper: newInvitationMapper(),
userRepo: repo.NewUserRepo(),
mailTmpl: infra.NewMailTemplate(),
orgMapper: newOrganizationMapper(),
config: config.GetConfig(),
}
}
func (svc *InvitationService) Create(opts InvitationCreateOptions, userID string) error {
for i := range opts.Emails {
opts.Emails[i] = strings.ToLower(opts.Emails[i])
}
user, err := svc.userRepo.Find(userID)
if err != nil {
return err
}
org, err := svc.orgCache.Get(opts.OrganizationID)
if err != nil {
return err
}
if err := svc.orgGuard.Authorize(user, org, model.PermissionOwner); err != nil {
return err
}
orgMembers, err := svc.orgRepo.GetMembers(opts.OrganizationID)
if err != nil {
return err
}
outgoingInvitations, err := svc.invitationRepo.GetOutgoing(opts.OrganizationID, userID)
if err != nil {
return err
}
var emails []string
/* Collect emails of non existing members and outgoing invitations */
for _, e := range opts.Emails {
existing := false
for _, u := range orgMembers {
if e == u.GetEmail() {
existing = true
break
}
}
for _, i := range outgoingInvitations {
if e == i.GetEmail() && i.GetStatus() == model.InvitationStatusPending {
existing = true
break
}
}
if !existing {
emails = append(emails, e)
}
}
/* Persist invitations */
invitations, err := svc.invitationRepo.Insert(repo.InvitationInsertOptions{
UserID: userID,
OrganizationID: opts.OrganizationID,
Emails: emails,
})
if err != nil {
return err
}
/* Send emails */
for _, inv := range invitations {
variables := map[string]string{
"USER_FULL_NAME": user.GetFullName(),
"ORGANIZATION_NAME": org.GetName(),
"UI_URL": svc.config.PublicUIURL,
}
_, err := svc.userRepo.FindByEmail(inv.GetEmail())
var templateName string
if err == nil {
templateName = "join-organization"
} else {
templateName = "signup-and-join-organization"
}
if err := svc.mailTmpl.Send(templateName, inv.GetEmail(), variables); err != nil {
return err
}
}
return nil
}
func (svc *InvitationService) GetIncoming(opts InvitationListOptions, userID string) (*InvitationList, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
invitations, err := svc.invitationRepo.GetIncoming(user.GetEmail())
if err != nil {
return nil, err
}
if opts.SortBy == "" {
opts.SortBy = SortByDateCreated
}
if opts.SortOrder == "" {
opts.SortOrder = SortOrderAsc
}
sorted := svc.doSorting(invitations, opts.SortBy, opts.SortOrder)
paged, totalElements, totalPages := svc.doPagination(sorted, opts.Page, opts.Size)
mapped, err := svc.invitationMapper.mapMany(paged, userID)
if err != nil {
return nil, err
}
return &InvitationList{
Data: mapped,
TotalPages: totalPages,
TotalElements: totalElements,
Page: opts.Page,
Size: uint(len(mapped)),
}, nil
}
func (svc *InvitationService) GetOutgoing(orgID string, opts InvitationListOptions, userID string) (*InvitationList, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
invitations, err := svc.invitationRepo.GetOutgoing(orgID, user.GetID())
if err != nil {
return nil, err
}
if opts.SortBy == "" {
opts.SortBy = SortByDateCreated
}
if opts.SortOrder == "" {
opts.SortOrder = SortOrderAsc
}
sorted := svc.doSorting(invitations, opts.SortBy, opts.SortOrder)
paged, totalElements, totalPages := svc.doPagination(sorted, opts.Page, opts.Size)
mapped, err := svc.invitationMapper.mapMany(paged, userID)
if err != nil {
return nil, err
}
return &InvitationList{
Data: mapped,
TotalPages: totalPages,
TotalElements: totalElements,
Page: opts.Page,
Size: uint(len(mapped)),
}, nil
}
func (svc *InvitationService) Accept(id string, userID string) error {
user, err := svc.userRepo.Find(userID)
if err != nil {
return err
}
invitation, err := svc.invitationRepo.Find(id)
if err != nil {
return err
}
if invitation.GetStatus() != model.InvitationStatusPending {
return errorpkg.NewCannotAcceptNonPendingInvitationError(invitation)
}
if user.GetEmail() != invitation.GetEmail() {
return errorpkg.NewUserNotAllowedToAcceptInvitationError(user, invitation)
}
org, err := svc.orgCache.Get(invitation.GetOrganizationID())
if err != nil {
return err
}
for _, u := range org.GetUsers() {
if u == userID {
return errorpkg.NewUserAlreadyMemberOfOrganizationError(user, org)
}
}
invitation.SetStatus(model.InvitationStatusAccepted)
if err := svc.invitationRepo.Save(invitation); err != nil {
return err
}
if err := svc.orgRepo.AddUser(invitation.GetOrganizationID(), userID); err != nil {
return err
}
if err := svc.orgRepo.GrantUserPermission(invitation.GetOrganizationID(), userID, model.PermissionViewer); err != nil {
return err
}
if _, err := svc.orgCache.Refresh(invitation.GetOrganizationID()); err != nil {
return err
}
return nil
}
func (svc *InvitationService) Decline(id string, userID string) error {
user, err := svc.userRepo.Find(userID)
if err != nil {
return err
}
invitation, err := svc.invitationRepo.Find(id)
if err != nil {
return err
}
if invitation.GetStatus() != model.InvitationStatusPending {
return errorpkg.NewCannotDeclineNonPendingInvitationError(invitation)
}
if user.GetEmail() != invitation.GetEmail() {
return errorpkg.NewUserNotAllowedToDeclineInvitationError(user, invitation)
}
invitation.SetStatus(model.InvitationStatusDeclined)
if err := svc.invitationRepo.Save(invitation); err != nil {
return err
}
return nil
}
func (svc *InvitationService) Resend(id string, userID string) error {
user, err := svc.userRepo.Find(userID)
if err != nil {
return err
}
invitation, err := svc.invitationRepo.Find(id)
if err != nil {
return err
}
if invitation.GetStatus() != model.InvitationStatusPending {
return errorpkg.NewCannotResendNonPendingInvitationError(invitation)
}
org, err := svc.orgCache.Get(invitation.GetOrganizationID())
if err != nil {
return err
}
variables := map[string]string{
"USER_FULL_NAME": user.GetFullName(),
"ORGANIZATION_NAME": org.GetName(),
"UI_URL": svc.config.PublicUIURL,
}
_, err = svc.userRepo.FindByEmail(invitation.GetEmail())
var templateName string
if err == nil {
templateName = "join-organization"
} else {
templateName = "signup-and-join-organization"
}
if err := svc.mailTmpl.Send(templateName, invitation.GetEmail(), variables); err != nil {
return err
}
return nil
}
func (svc *InvitationService) Delete(id string, userID string) error {
invitation, err := svc.invitationRepo.Find(id)
if err != nil {
return err
}
if userID != invitation.GetOwnerID() {
user, err := svc.userRepo.Find(userID)
if err != nil {
return err
}
return errorpkg.NewUserNotAllowedToDeleteInvitationError(user, invitation)
}
if err := svc.invitationRepo.Delete(invitation.GetID()); err != nil {
return err
}
return nil
}
func (svc *InvitationService) doSorting(data []model.Invitation, sortBy string, sortOrder string) []model.Invitation {
if sortBy == SortByEmail {
sort.Slice(data, func(i, j int) bool {
if sortOrder == SortOrderDesc {
return data[i].GetEmail() > data[j].GetEmail()
} else {
return data[i].GetEmail() < data[j].GetEmail()
}
})
return data
} else if sortBy == SortByDateCreated {
sort.Slice(data, func(i, j int) bool {
a, _ := time.Parse(time.RFC3339, data[i].GetCreateTime())
b, _ := time.Parse(time.RFC3339, data[j].GetCreateTime())
if sortOrder == SortOrderDesc {
return a.UnixMilli() > b.UnixMilli()
} else {
return a.UnixMilli() < b.UnixMilli()
}
})
return data
} else if sortBy == SortByDateModified {
sort.Slice(data, func(i, j int) bool {
if data[i].GetUpdateTime() != nil && data[j].GetUpdateTime() != nil {
a, _ := time.Parse(time.RFC3339, *data[i].GetUpdateTime())
b, _ := time.Parse(time.RFC3339, *data[j].GetUpdateTime())
if sortOrder == SortOrderDesc {
return a.UnixMilli() > b.UnixMilli()
} else {
return a.UnixMilli() < b.UnixMilli()
}
} else {
return false
}
})
return data
}
return data
}
func (svc *InvitationService) doPagination(data []model.Invitation, page, size uint) ([]model.Invitation, uint, uint) {
totalElements := uint(len(data))
totalPages := (totalElements + size - 1) / size
if page > totalPages {
return nil, totalElements, totalPages
}
startIndex := (page - 1) * size
endIndex := startIndex + size
if endIndex > totalElements {
endIndex = totalElements
}
pageData := data[startIndex:endIndex]
return pageData, totalElements, totalPages
}
type invitationMapper struct {
orgCache *cache.OrganizationCache
userRepo repo.UserRepo
userMapper *userMapper
orgMapper *organizationMapper
}
func newInvitationMapper() *invitationMapper {
return &invitationMapper{
orgCache: cache.NewOrganizationCache(),
userRepo: repo.NewUserRepo(),
userMapper: newUserMapper(),
orgMapper: newOrganizationMapper(),
}
}
func (mp *invitationMapper) mapOne(m model.Invitation, userID string) (*Invitation, error) {
owner, err := mp.userRepo.Find(m.GetOwnerID())
if err != nil {
return nil, err
}
org, err := mp.orgCache.Get(m.GetOrganizationID())
if err != nil {
return nil, err
}
v, err := mp.orgMapper.mapOne(org, userID)
if err != nil {
return nil, err
}
return &Invitation{
ID: m.GetID(),
Owner: mp.userMapper.mapOne(owner),
Email: m.GetEmail(),
Organization: v,
Status: m.GetStatus(),
CreateTime: m.GetCreateTime(),
UpdateTime: m.GetUpdateTime(),
}, nil
}
func (mp *invitationMapper) mapMany(invitations []model.Invitation, userID string) ([]*Invitation, error) {
res := make([]*Invitation, 0)
for _, m := range invitations {
v, err := mp.mapOne(m, userID)
if err != nil {
return nil, err
}
res = append(res, v)
}
return res, nil
}

View File

@@ -0,0 +1,45 @@
package service
import "voltaserve/repo"
type Notification struct {
Type string `json:"type"`
Body interface{} `json:"body"`
}
type NotificationService struct {
userRepo repo.UserRepo
invitationRepo repo.InvitationRepo
invitationMapper *invitationMapper
}
func NewNotificationService() *NotificationService {
return &NotificationService{
userRepo: repo.NewUserRepo(),
invitationRepo: repo.NewInvitationRepo(),
invitationMapper: newInvitationMapper(),
}
}
func (svc *NotificationService) GetAll(userID string) ([]*Notification, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
invitations, err := svc.invitationRepo.GetIncoming(user.GetEmail())
if err != nil {
return nil, err
}
notifications := make([]*Notification, 0)
for _, inv := range invitations {
v, err := svc.invitationMapper.mapOne(inv, userID)
if err != nil {
return nil, err
}
notifications = append(notifications, &Notification{
Type: "new_invitation",
Body: &v,
})
}
return notifications, nil
}

View File

@@ -0,0 +1,418 @@
package service
import (
"sort"
"time"
"voltaserve/cache"
"voltaserve/config"
"voltaserve/errorpkg"
"voltaserve/guard"
"voltaserve/helper"
"voltaserve/model"
"voltaserve/repo"
"voltaserve/search"
"github.com/gofiber/fiber/v2/log"
)
type Organization struct {
ID string `json:"id"`
Name string `json:"name"`
Image *string `json:"image,omitempty"`
Permission string `json:"permission"`
CreateTime string `json:"createTime"`
UpdateTime *string `json:"updateTime,omitempty"`
}
type OrganizationList struct {
Data []*Organization `json:"data"`
TotalPages uint `json:"totalPages"`
TotalElements uint `json:"totalElements"`
Page uint `json:"page"`
Size uint `json:"size"`
}
type OrganizationCreateOptions struct {
Name string `json:"name" validate:"required,max=255"`
Image *string `json:"image"`
}
type OrganizationListOptions struct {
Query string
Page uint
Size uint
SortBy string
SortOrder string
}
type OrganizationUpdateNameOptions struct {
Name string `json:"name" validate:"required,max=255"`
}
type OrganizationUpdateImageOptions struct {
Image string `json:"image" validate:"required,base64"`
}
type OrganizationRemoveMemberOptions struct {
UserID string `json:"userId" validate:"required"`
}
type OrganizationService struct {
orgRepo repo.OrganizationRepo
orgCache *cache.OrganizationCache
orgGuard *guard.OrganizationGuard
orgMapper *organizationMapper
orgSearch *search.OrganizationSearch
userRepo repo.UserRepo
userSearch *search.UserSearch
userMapper *userMapper
groupRepo repo.GroupRepo
groupService *GroupService
groupMapper *groupMapper
config config.Config
}
func NewOrganizationService() *OrganizationService {
return &OrganizationService{
orgRepo: repo.NewOrganizationRepo(),
orgCache: cache.NewOrganizationCache(),
orgGuard: guard.NewOrganizationGuard(),
orgSearch: search.NewOrganizationSearch(),
orgMapper: newOrganizationMapper(),
userRepo: repo.NewUserRepo(),
userSearch: search.NewUserSearch(),
groupRepo: repo.NewGroupRepo(),
groupService: NewGroupService(),
groupMapper: newGroupMapper(),
userMapper: newUserMapper(),
config: config.GetConfig(),
}
}
func (svc *OrganizationService) Create(opts OrganizationCreateOptions, userID string) (*Organization, error) {
org, err := svc.orgRepo.Insert(repo.OrganizationInsertOptions{
ID: helper.NewID(),
Name: opts.Name,
})
if err != nil {
return nil, err
}
if err := svc.orgRepo.GrantUserPermission(org.GetID(), userID, model.PermissionOwner); err != nil {
return nil, err
}
org, err = svc.orgRepo.Find(org.GetID())
if err != nil {
return nil, err
}
if err := svc.orgSearch.Index([]model.Organization{org}); err != nil {
return nil, err
}
if err := svc.orgCache.Set(org); err != nil {
return nil, nil
}
res, err := svc.orgMapper.mapOne(org, userID)
if err != nil {
return nil, err
}
return res, nil
}
func (svc *OrganizationService) Find(id string, userID string) (*Organization, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
org, err := svc.orgCache.Get(id)
if err != nil {
return nil, err
}
if err := svc.orgGuard.Authorize(user, org, model.PermissionViewer); err != nil {
return nil, err
}
res, err := svc.orgMapper.mapOne(org, userID)
if err != nil {
return nil, err
}
return res, nil
}
func (svc *OrganizationService) List(opts OrganizationListOptions, userID string) (*OrganizationList, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
var authorized []model.Organization
if opts.Query == "" {
ids, err := svc.orgRepo.GetIDs()
if err != nil {
return nil, err
}
authorized, err = svc.doAuthorizationByIDs(ids, user)
if err != nil {
return nil, err
}
} else {
orgs, err := svc.orgSearch.Query(opts.Query)
if err != nil {
return nil, err
}
authorized, err = svc.doAuthorization(orgs, user)
if err != nil {
return nil, err
}
}
if opts.SortBy == "" {
opts.SortBy = SortByDateCreated
}
if opts.SortOrder == "" {
opts.SortOrder = SortOrderAsc
}
sorted := svc.doSorting(authorized, opts.SortBy, opts.SortOrder)
paged, totalElements, totalPages := svc.doPagination(sorted, opts.Page, opts.Size)
mapped, err := svc.orgMapper.mapMany(paged, userID)
if err != nil {
return nil, err
}
return &OrganizationList{
Data: mapped,
TotalPages: totalPages,
TotalElements: totalElements,
Page: opts.Page,
Size: uint(len(mapped)),
}, nil
}
func (svc *OrganizationService) UpdateName(id string, name string, userID string) (*Organization, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
org, err := svc.orgCache.Get(id)
if err != nil {
return nil, err
}
if err := svc.orgGuard.Authorize(user, org, model.PermissionEditor); err != nil {
return nil, err
}
org.SetName(name)
if err := svc.orgRepo.Save(org); err != nil {
return nil, err
}
if err := svc.orgSearch.Update([]model.Organization{org}); err != nil {
return nil, err
}
err = svc.orgCache.Set(org)
if err != nil {
return nil, err
}
res, err := svc.orgMapper.mapOne(org, userID)
if err != nil {
return nil, err
}
return res, nil
}
func (svc *OrganizationService) Delete(id string, userID string) error {
user, err := svc.userRepo.Find(userID)
if err != nil {
return err
}
org, err := svc.orgCache.Get(id)
if err != nil {
return err
}
if err := svc.orgGuard.Authorize(user, org, model.PermissionOwner); err != nil {
return err
}
if err := svc.orgRepo.Delete(id); err != nil {
return err
}
if err := svc.orgCache.Delete(org.GetID()); err != nil {
return err
}
if err := svc.orgSearch.Delete([]string{org.GetID()}); err != nil {
return err
}
return nil
}
func (svc *OrganizationService) RemoveMember(id string, memberID string, userID string) error {
user, err := svc.userRepo.Find(userID)
if err != nil {
return err
}
member, err := svc.userRepo.Find(memberID)
if err != nil {
return err
}
org, err := svc.orgCache.Get(id)
if err != nil {
return err
}
/* Make sure member is not the last remaining owner of the organization */
ownerCount, err := svc.orgRepo.GetOwnerCount(org.GetID())
if err != nil {
return err
}
if svc.orgGuard.IsAuthorized(member, org, model.PermissionOwner) && ownerCount == 1 {
return errorpkg.NewCannotRemoveLastRemainingOwnerOfOrganizationError(org.GetID())
}
if userID != member.GetID() {
if err := svc.orgGuard.Authorize(user, org, model.PermissionOwner); err != nil {
return err
}
}
/* Remove member from all groups belonging to this organization */
groupsIDs, err := svc.groupRepo.GetIDsForOrganization(org.GetID())
if err != nil {
return err
}
for _, groupID := range groupsIDs {
if err := svc.groupService.RemoveMemberUnauthorized(groupID, member.GetID()); err != nil {
log.Error(err)
}
}
if err := svc.orgRepo.RevokeUserPermission(id, member.GetID()); err != nil {
return err
}
if err := svc.orgRepo.RemoveMember(id, member.GetID()); err != nil {
return err
}
if _, err := svc.orgCache.Refresh(org.GetID()); err != nil {
return err
}
return nil
}
func (svc *OrganizationService) doAuthorization(data []model.Organization, user model.User) ([]model.Organization, error) {
var res []model.Organization
for _, o := range data {
if svc.orgGuard.IsAuthorized(user, o, model.PermissionViewer) {
res = append(res, o)
}
}
return res, nil
}
func (svc *OrganizationService) doAuthorizationByIDs(ids []string, user model.User) ([]model.Organization, error) {
var res []model.Organization
for _, id := range ids {
var o model.Organization
o, err := svc.orgCache.Get(id)
if err != nil {
return nil, err
}
if svc.orgGuard.IsAuthorized(user, o, model.PermissionViewer) {
res = append(res, o)
}
}
return res, nil
}
func (svc *OrganizationService) doSorting(data []model.Organization, sortBy string, sortOrder string) []model.Organization {
if sortBy == SortByName {
sort.Slice(data, func(i, j int) bool {
if sortOrder == SortOrderDesc {
return data[i].GetName() > data[j].GetName()
} else {
return data[i].GetName() < data[j].GetName()
}
})
return data
} else if sortBy == SortByDateCreated {
sort.Slice(data, func(i, j int) bool {
a, _ := time.Parse(time.RFC3339, data[i].GetCreateTime())
b, _ := time.Parse(time.RFC3339, data[j].GetCreateTime())
if sortOrder == SortOrderDesc {
return a.UnixMilli() > b.UnixMilli()
} else {
return a.UnixMilli() < b.UnixMilli()
}
})
return data
} else if sortBy == SortByDateModified {
sort.Slice(data, func(i, j int) bool {
if data[i].GetUpdateTime() != nil && data[j].GetUpdateTime() != nil {
a, _ := time.Parse(time.RFC3339, *data[i].GetUpdateTime())
b, _ := time.Parse(time.RFC3339, *data[j].GetUpdateTime())
if sortOrder == SortOrderDesc {
return a.UnixMilli() > b.UnixMilli()
} else {
return a.UnixMilli() < b.UnixMilli()
}
} else {
return false
}
})
return data
}
return data
}
func (svc *OrganizationService) doPagination(data []model.Organization, page, size uint) ([]model.Organization, uint, uint) {
totalElements := uint(len(data))
totalPages := (totalElements + size - 1) / size
if page > totalPages {
return nil, totalElements, totalPages
}
startIndex := (page - 1) * size
endIndex := startIndex + size
if endIndex > totalElements {
endIndex = totalElements
}
pageData := data[startIndex:endIndex]
return pageData, totalElements, totalPages
}
type organizationMapper struct {
groupCache *cache.GroupCache
}
func newOrganizationMapper() *organizationMapper {
return &organizationMapper{
groupCache: cache.NewGroupCache(),
}
}
func (mp *organizationMapper) mapOne(m model.Organization, userID string) (*Organization, error) {
res := &Organization{
ID: m.GetID(),
Name: m.GetName(),
CreateTime: m.GetCreateTime(),
UpdateTime: m.GetUpdateTime(),
}
res.Permission = ""
for _, p := range m.GetUserPermissions() {
if p.GetUserID() == userID && model.GetPermissionWeight(p.GetValue()) > model.GetPermissionWeight(res.Permission) {
res.Permission = p.GetValue()
}
}
for _, p := range m.GetGroupPermissions() {
g, err := mp.groupCache.Get(p.GetGroupID())
if err != nil {
return nil, err
}
for _, u := range g.GetUsers() {
if u == userID && model.GetPermissionWeight(p.GetValue()) > model.GetPermissionWeight(res.Permission) {
res.Permission = p.GetValue()
}
}
}
return res, nil
}
func (mp *organizationMapper) mapMany(orgs []model.Organization, userID string) ([]*Organization, error) {
res := make([]*Organization, 0)
for _, f := range orgs {
v, err := mp.mapOne(f, userID)
if err != nil {
return nil, err
}
res = append(res, v)
}
return res, nil
}

View File

@@ -0,0 +1,142 @@
package service
import (
"voltaserve/cache"
"voltaserve/guard"
"voltaserve/model"
"voltaserve/repo"
)
type StorageUsage struct {
Bytes int64 `json:"bytes"`
MaxBytes int64 `json:"maxBytes"`
Percentage int `json:"percentage"`
}
type StorageService struct {
workspaceRepo repo.WorkspaceRepo
workspaceCache *cache.WorkspaceCache
workspaceGuard *guard.WorkspaceGuard
fileRepo repo.FileRepo
fileCache *cache.FileCache
fileGuard *guard.FileGuard
storageMapper *storageMapper
userRepo repo.UserRepo
}
func NewStorageService() *StorageService {
return &StorageService{
workspaceRepo: repo.NewWorkspaceRepo(),
workspaceCache: cache.NewWorkspaceCache(),
workspaceGuard: guard.NewWorkspaceGuard(),
fileRepo: repo.NewFileRepo(),
fileCache: cache.NewFileCache(),
fileGuard: guard.NewFileGuard(),
storageMapper: newStorageMapper(),
userRepo: repo.NewUserRepo(),
}
}
func (svc *StorageService) GetAccountUsage(userID string) (*StorageUsage, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
ids, err := svc.workspaceRepo.GetIDs()
if err != nil {
return nil, err
}
workspaces := []model.Workspace{}
for _, id := range ids {
var workspace model.Workspace
workspace, err = svc.workspaceCache.Get(id)
if err != nil {
return nil, err
}
if svc.workspaceGuard.IsAuthorized(user, workspace, model.PermissionOwner) {
workspaces = append(workspaces, workspace)
}
}
var maxBytes int64 = 0
var b int64 = 0
for _, w := range workspaces {
root, err := svc.fileCache.Get(w.GetRootID())
if err != nil {
return nil, err
}
size, err := svc.fileRepo.GetSize(root.GetID())
if err != nil {
return nil, err
}
b = b + size
maxBytes = maxBytes + w.GetStorageCapacity()
}
return svc.storageMapper.mapStorageUsage(b, maxBytes), nil
}
func (svc *StorageService) GetWorkspaceUsage(workspaceID string, userID string) (*StorageUsage, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
workspace, err := svc.workspaceCache.Get(workspaceID)
if err != nil {
return nil, err
}
if err = svc.workspaceGuard.Authorize(user, workspace, model.PermissionViewer); err != nil {
return nil, err
}
root, err := svc.fileCache.Get(workspace.GetRootID())
if err != nil {
return nil, err
}
if err = svc.fileGuard.Authorize(user, root, model.PermissionViewer); err != nil {
return nil, err
}
size, err := svc.fileRepo.GetSize(root.GetID())
if err != nil {
return nil, err
}
return svc.storageMapper.mapStorageUsage(size, workspace.GetStorageCapacity()), nil
}
func (svc *StorageService) GetFileUsage(fileID string, userID string) (*StorageUsage, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
file, err := svc.fileCache.Get(fileID)
if err != nil {
return nil, err
}
if err = svc.fileGuard.Authorize(user, file, model.PermissionViewer); err != nil {
return nil, err
}
size, err := svc.fileRepo.GetSize(file.GetID())
if err != nil {
return nil, err
}
workspace, err := svc.workspaceCache.Get(file.GetWorkspaceID())
if err != nil {
return nil, err
}
return svc.storageMapper.mapStorageUsage(size, workspace.GetStorageCapacity()), nil
}
type storageMapper struct {
}
func newStorageMapper() *storageMapper {
return &storageMapper{}
}
func (mp *storageMapper) mapStorageUsage(byteCount int64, maxBytes int64) *StorageUsage {
res := StorageUsage{
Bytes: byteCount,
MaxBytes: maxBytes,
}
if maxBytes != 0 {
res.Percentage = int(byteCount * 100 / maxBytes)
}
return &res
}

View File

@@ -0,0 +1,247 @@
package service
import (
"sort"
"voltaserve/cache"
"voltaserve/config"
"voltaserve/guard"
"voltaserve/model"
"voltaserve/repo"
"voltaserve/search"
)
type User struct {
ID string `json:"id"`
FullName string `json:"fullName"`
Picture *string `json:"picture,omitempty"`
Email string `json:"email"`
Username string `json:"username"`
CreateTime string `json:"createTime"`
UpdateTime *string `json:"updateTime"`
}
type UserList struct {
Data []*User `json:"data"`
TotalPages uint `json:"totalPages"`
TotalElements uint `json:"totalElements"`
Page uint `json:"page"`
Size uint `json:"size"`
}
type UserListOptions struct {
Query string
OrganizationID string
GroupID string
NonGroupMembersOnly bool
SortBy string
SortOrder string
Page uint
Size uint
}
type UserService struct {
userRepo repo.UserRepo
userMapper *userMapper
userSearch *search.UserSearch
orgRepo repo.OrganizationRepo
orgCache *cache.OrganizationCache
orgGuard *guard.OrganizationGuard
groupRepo repo.GroupRepo
groupGuard *guard.GroupGuard
groupCache *cache.GroupCache
config config.Config
}
func NewUserService() *UserService {
return &UserService{
userRepo: repo.NewUserRepo(),
userMapper: newUserMapper(),
userSearch: search.NewUserSearch(),
orgRepo: repo.NewOrganizationRepo(),
orgCache: cache.NewOrganizationCache(),
orgGuard: guard.NewOrganizationGuard(),
groupRepo: repo.NewGroupRepo(),
groupGuard: guard.NewGroupGuard(),
groupCache: cache.NewGroupCache(),
config: config.GetConfig(),
}
}
func (svc *UserService) List(opts UserListOptions, userID string) (*UserList, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
if opts.OrganizationID == "" && opts.GroupID == "" {
return &UserList{
Data: []*User{},
TotalPages: 1,
TotalElements: 0,
Page: 1,
Size: 0,
}, nil
}
var org model.Organization
if opts.OrganizationID != "" {
org, err = svc.orgCache.Get(opts.OrganizationID)
if err != nil {
return nil, err
}
if err := svc.orgGuard.Authorize(user, org, model.PermissionViewer); err != nil {
return nil, err
}
}
var group model.Group
if opts.GroupID != "" {
group, err = svc.groupCache.Get(opts.GroupID)
if err != nil {
return nil, err
}
if err := svc.groupGuard.Authorize(user, group, model.PermissionViewer); err != nil {
return nil, err
}
}
res := []model.User{}
if opts.Query == "" {
if opts.OrganizationID != "" && opts.GroupID != "" && opts.NonGroupMembersOnly {
orgMembers, err := svc.orgRepo.GetMembers(group.GetOrganizationID())
if err != nil {
return nil, err
}
groupMembers, err := svc.groupRepo.GetMembers(opts.GroupID)
if err != nil {
return nil, err
}
for _, om := range orgMembers {
found := false
for _, tm := range groupMembers {
if om.GetID() == tm.GetID() {
found = true
break
}
}
if !found {
res = append(res, om)
}
}
} else if opts.OrganizationID != "" {
res, err = svc.orgRepo.GetMembers(opts.OrganizationID)
if err != nil {
return nil, err
}
} else if opts.GroupID != "" {
res, err = svc.groupRepo.GetMembers(opts.GroupID)
if err != nil {
return nil, err
}
}
} else {
users, err := svc.userSearch.Query(opts.Query)
if err != nil {
return nil, err
}
var members []model.User
if opts.OrganizationID != "" {
members, err = svc.orgRepo.GetMembers(opts.OrganizationID)
if err != nil {
return nil, err
}
} else if opts.GroupID != "" {
members, err = svc.groupRepo.GetMembers(opts.GroupID)
if err != nil {
return nil, err
}
}
for _, m := range members {
for _, u := range users {
if u.GetID() == m.GetID() {
res = append(res, m)
}
}
}
}
if opts.SortBy == "" {
opts.SortBy = SortByDateCreated
}
if opts.SortOrder == "" {
opts.SortOrder = SortOrderAsc
}
sorted := svc.doSorting(res, opts.SortBy, opts.SortOrder)
paged, totalElements, totalPages := svc.doPagination(sorted, opts.Page, opts.Size)
mapped, err := svc.userMapper.mapMany(paged)
if err != nil {
return nil, err
}
return &UserList{
Data: mapped,
TotalPages: totalPages,
TotalElements: totalElements,
Page: opts.Page,
Size: uint(len(mapped)),
}, nil
}
func (svc *UserService) doSorting(data []model.User, sortBy string, sortOrder string) []model.User {
if sortBy == SortByEmail {
sort.Slice(data, func(i, j int) bool {
if sortOrder == SortOrderDesc {
return data[i].GetEmail() > data[j].GetEmail()
} else {
return data[i].GetEmail() < data[j].GetEmail()
}
})
return data
} else if sortBy == SortByFullName {
sort.Slice(data, func(i, j int) bool {
if sortOrder == SortOrderDesc {
return data[i].GetFullName() > data[j].GetFullName()
} else {
return data[i].GetFullName() < data[j].GetFullName()
}
})
return data
}
return data
}
func (svc *UserService) doPagination(data []model.User, page, size uint) ([]model.User, uint, uint) {
totalElements := uint(len(data))
totalPages := (totalElements + size - 1) / size
if page > totalPages {
return nil, totalElements, totalPages
}
startIndex := (page - 1) * size
endIndex := startIndex + size
if endIndex > totalElements {
endIndex = totalElements
}
pageData := data[startIndex:endIndex]
return pageData, totalElements, totalPages
}
type userMapper struct {
}
func newUserMapper() *userMapper {
return &userMapper{}
}
func (mp *userMapper) mapOne(user model.User) *User {
return &User{
ID: user.GetID(),
FullName: user.GetFullName(),
Picture: user.GetPicture(),
Email: user.GetEmail(),
Username: user.GetUsername(),
CreateTime: user.GetCreateTime(),
UpdateTime: user.GetUpdateTime(),
}
}
func (mp *userMapper) mapMany(users []model.User) ([]*User, error) {
res := []*User{}
for _, u := range users {
res = append(res, mp.mapOne(u))
}
return res, nil
}

View File

@@ -0,0 +1,502 @@
package service
import (
"sort"
"strings"
"time"
"voltaserve/cache"
"voltaserve/config"
"voltaserve/errorpkg"
"voltaserve/guard"
"voltaserve/helper"
"voltaserve/infra"
"voltaserve/model"
"voltaserve/repo"
"voltaserve/search"
"github.com/google/uuid"
)
type Workspace struct {
ID string `json:"id"`
Image *string `json:"image,omitempty"`
Name string `json:"name"`
RootID string `json:"rootId,omitempty"`
StorageCapacity int64 `json:"storageCapacity"`
Permission string `json:"permission"`
Organization Organization `json:"organization"`
CreateTime string `json:"createTime"`
UpdateTime *string `json:"updateTime,omitempty"`
}
type WorkspaceList struct {
Data []*Workspace `json:"data"`
TotalPages uint `json:"totalPages"`
TotalElements uint `json:"totalElements"`
Page uint `json:"page"`
Size uint `json:"size"`
}
type WorkspaceCreateOptions struct {
Name string `json:"name" validate:"required,max=255"`
Image *string `json:"image"`
OrganizationID string `json:"organizationId" validate:"required"`
StorageCapacity int64 `json:"storageCapacity"`
}
type WorkspaceListOptions struct {
Query string
Page uint
Size uint
SortBy string
SortOrder string
}
type WorkspaceUpdateNameOptions struct {
Name string `json:"name" validate:"required,max=255"`
}
type WorkspaceUpdateStorageCapacityOptions struct {
StorageCapacity int64 `json:"storageCapacity" validate:"required,min=1"`
}
type WorkspaceUpdateIsAutomaticOCREnabledOptions struct {
IsEnabled bool `json:"isEnabled" validate:"required"`
}
type WorkspaceService struct {
workspaceRepo repo.WorkspaceRepo
workspaceCache *cache.WorkspaceCache
workspaceGuard *guard.WorkspaceGuard
workspaceSearch *search.WorkspaceSearch
workspaceMapper *workspaceMapper
fileRepo repo.FileRepo
fileCache *cache.FileCache
fileGuard *guard.FileGuard
fileMapper *FileMapper
userRepo repo.UserRepo
s3 *infra.S3Manager
config config.Config
}
func NewWorkspaceService() *WorkspaceService {
return &WorkspaceService{
workspaceRepo: repo.NewWorkspaceRepo(),
workspaceCache: cache.NewWorkspaceCache(),
workspaceSearch: search.NewWorkspaceSearch(),
workspaceGuard: guard.NewWorkspaceGuard(),
workspaceMapper: newWorkspaceMapper(),
fileRepo: repo.NewFileRepo(),
fileCache: cache.NewFileCache(),
fileGuard: guard.NewFileGuard(),
fileMapper: NewFileMapper(),
userRepo: repo.NewUserRepo(),
s3: infra.NewS3Manager(),
config: config.GetConfig(),
}
}
func (svc *WorkspaceService) Create(opts WorkspaceCreateOptions, userID string) (*Workspace, error) {
id := helper.NewID()
bucket := strings.ReplaceAll(uuid.NewString(), "-", "")
if err := svc.s3.CreateBucket(bucket); err != nil {
return nil, err
}
if opts.StorageCapacity == 0 {
opts.StorageCapacity = svc.config.Defaults.WorkspaceStorageCapacityBytes
}
workspace, err := svc.workspaceRepo.Insert(repo.WorkspaceInsertOptions{
ID: id,
Name: opts.Name,
StorageCapacity: opts.StorageCapacity,
OrganizationID: opts.OrganizationID,
Image: opts.Image,
Bucket: bucket,
})
if err != nil {
return nil, err
}
if err := svc.workspaceRepo.GrantUserPermission(workspace.GetID(), userID, model.PermissionOwner); err != nil {
return nil, err
}
workspace, err = svc.workspaceRepo.Find(workspace.GetID())
if err != nil {
return nil, err
}
root, err := svc.fileRepo.Insert(repo.FileInsertOptions{
Name: "root",
WorkspaceID: workspace.GetID(),
Type: model.FileTypeFolder,
})
if err != nil {
return nil, err
}
if err := svc.fileRepo.GrantUserPermission(root.GetID(), userID, model.PermissionOwner); err != nil {
return nil, err
}
if err = svc.workspaceRepo.UpdateRootID(workspace.GetID(), root.GetID()); err != nil {
return nil, err
}
if workspace, err = svc.workspaceRepo.Find(workspace.GetID()); err != nil {
return nil, err
}
if err = svc.workspaceSearch.Index([]model.Workspace{workspace}); err != nil {
return nil, err
}
if root, err = svc.fileRepo.Find(root.GetID()); err != nil {
return nil, err
}
if err := svc.fileCache.Set(root); err != nil {
return nil, err
}
if err = svc.workspaceCache.Set(workspace); err != nil {
return nil, err
}
res, err := svc.workspaceMapper.mapOne(workspace, userID)
if err != nil {
return nil, err
}
return res, nil
}
func (svc *WorkspaceService) Find(id string, userID string) (*Workspace, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
workspace, err := svc.workspaceCache.Get(id)
if err != nil {
return nil, err
}
if err = svc.workspaceGuard.Authorize(user, workspace, model.PermissionViewer); err != nil {
return nil, err
}
res, err := svc.workspaceMapper.mapOne(workspace, userID)
if err != nil {
return nil, err
}
return res, nil
}
func (svc *WorkspaceService) List(opts WorkspaceListOptions, userID string) (*WorkspaceList, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
var authorized []model.Workspace
if opts.Query == "" {
ids, err := svc.workspaceRepo.GetIDs()
if err != nil {
return nil, err
}
authorized, err = svc.doAuthorizationByIDs(ids, user)
if err != nil {
return nil, err
}
} else {
workspaces, err := svc.workspaceSearch.Query(opts.Query)
if err != nil {
return nil, err
}
authorized, err = svc.doAuthorization(workspaces, user)
if err != nil {
return nil, err
}
}
if opts.SortBy == "" {
opts.SortBy = SortByDateCreated
}
if opts.SortOrder == "" {
opts.SortOrder = SortOrderAsc
}
sorted := svc.doSorting(authorized, opts.SortBy, opts.SortOrder)
paged, totalElements, totalPages := svc.doPagination(sorted, opts.Page, opts.Size)
mapped, err := svc.workspaceMapper.mapMany(paged, userID)
if err != nil {
return nil, err
}
return &WorkspaceList{
Data: mapped,
TotalPages: totalPages,
TotalElements: totalElements,
Page: opts.Page,
Size: uint(len(mapped)),
}, nil
}
func (svc *WorkspaceService) UpdateName(id string, name string, userID string) (*Workspace, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
workspace, err := svc.workspaceCache.Get(id)
if err != nil {
return nil, err
}
if err = svc.workspaceGuard.Authorize(user, workspace, model.PermissionEditor); err != nil {
return nil, err
}
if workspace, err = svc.workspaceRepo.UpdateName(id, name); err != nil {
return nil, err
}
if err = svc.workspaceSearch.Update([]model.Workspace{workspace}); err != nil {
return nil, err
}
if err = svc.workspaceCache.Set(workspace); err != nil {
return nil, err
}
res, err := svc.workspaceMapper.mapOne(workspace, userID)
if err != nil {
return nil, err
}
return res, nil
}
func (svc *WorkspaceService) UpdateStorageCapacity(id string, storageCapacity int64, userID string) (*Workspace, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
workspace, err := svc.workspaceCache.Get(id)
if err != nil {
return nil, err
}
if err = svc.workspaceGuard.Authorize(user, workspace, model.PermissionEditor); err != nil {
return nil, err
}
size, err := svc.fileRepo.GetSize(workspace.GetRootID())
if err != nil {
return nil, err
}
if storageCapacity < size {
return nil, errorpkg.NewInsufficientStorageCapacityError()
}
if workspace, err = svc.workspaceRepo.UpdateStorageCapacity(id, storageCapacity); err != nil {
return nil, err
}
if err = svc.workspaceSearch.Update([]model.Workspace{workspace}); err != nil {
return nil, err
}
if err = svc.workspaceCache.Set(workspace); err != nil {
return nil, err
}
res, err := svc.workspaceMapper.mapOne(workspace, userID)
if err != nil {
return nil, err
}
return res, nil
}
func (svc *WorkspaceService) Delete(id string, userID string) error {
user, err := svc.userRepo.Find(userID)
if err != nil {
return err
}
workspace, err := svc.workspaceCache.Get(id)
if err != nil {
return err
}
if err = svc.workspaceGuard.Authorize(user, workspace, model.PermissionOwner); err != nil {
return err
}
if workspace, err = svc.workspaceRepo.Find(id); err != nil {
return err
}
if err = svc.workspaceRepo.Delete(id); err != nil {
return err
}
if err = svc.workspaceSearch.Delete([]string{workspace.GetID()}); err != nil {
return err
}
if err = svc.workspaceCache.Delete(id); err != nil {
return err
}
if err = svc.s3.RemoveBucket(workspace.GetBucket()); err != nil {
return err
}
return nil
}
func (svc *WorkspaceService) HasEnoughSpaceForByteSize(id string, byteSize int64) (bool, error) {
workspace, err := svc.workspaceRepo.Find(id)
if err != nil {
return false, err
}
root, err := svc.fileRepo.Find(workspace.GetRootID())
if err != nil {
return false, err
}
usage, err := svc.fileRepo.GetSize(root.GetID())
if err != nil {
return false, err
}
expectedUsage := usage + byteSize
if expectedUsage > workspace.GetStorageCapacity() {
return false, err
}
return true, nil
}
func (svc *WorkspaceService) findAll(userID string) ([]*Workspace, error) {
user, err := svc.userRepo.Find(userID)
if err != nil {
return nil, err
}
ids, err := svc.workspaceRepo.GetIDs()
if err != nil {
return nil, err
}
authorized, err := svc.doAuthorizationByIDs(ids, user)
if err != nil {
return nil, err
}
mapped, err := svc.workspaceMapper.mapMany(authorized, userID)
if err != nil {
return nil, err
}
return mapped, nil
}
func (svc *WorkspaceService) doAuthorization(data []model.Workspace, user model.User) ([]model.Workspace, error) {
var res []model.Workspace
for _, w := range data {
if svc.workspaceGuard.IsAuthorized(user, w, model.PermissionViewer) {
res = append(res, w)
}
}
return res, nil
}
func (svc *WorkspaceService) doAuthorizationByIDs(ids []string, user model.User) ([]model.Workspace, error) {
var res []model.Workspace
for _, id := range ids {
var w model.Workspace
w, err := svc.workspaceCache.Get(id)
if err != nil {
return nil, err
}
if svc.workspaceGuard.IsAuthorized(user, w, model.PermissionViewer) {
res = append(res, w)
}
}
return res, nil
}
func (svc *WorkspaceService) doSorting(data []model.Workspace, sortBy string, sortOrder string) []model.Workspace {
if sortBy == SortByName {
sort.Slice(data, func(i, j int) bool {
if sortOrder == SortOrderDesc {
return data[i].GetName() > data[j].GetName()
} else {
return data[i].GetName() < data[j].GetName()
}
})
return data
} else if sortBy == SortByDateCreated {
sort.Slice(data, func(i, j int) bool {
a, _ := time.Parse(time.RFC3339, data[i].GetCreateTime())
b, _ := time.Parse(time.RFC3339, data[j].GetCreateTime())
if sortOrder == SortOrderDesc {
return a.UnixMilli() > b.UnixMilli()
} else {
return a.UnixMilli() < b.UnixMilli()
}
})
return data
} else if sortBy == SortByDateModified {
sort.Slice(data, func(i, j int) bool {
if data[i].GetUpdateTime() != nil && data[j].GetUpdateTime() != nil {
a, _ := time.Parse(time.RFC3339, *data[i].GetUpdateTime())
b, _ := time.Parse(time.RFC3339, *data[j].GetUpdateTime())
if sortOrder == SortOrderDesc {
return a.UnixMilli() > b.UnixMilli()
} else {
return a.UnixMilli() < b.UnixMilli()
}
} else {
return false
}
})
return data
}
return data
}
func (svc *WorkspaceService) doPagination(data []model.Workspace, page, size uint) ([]model.Workspace, uint, uint) {
totalElements := uint(len(data))
totalPages := (totalElements + size - 1) / size
if page > totalPages {
return nil, totalElements, totalPages
}
startIndex := (page - 1) * size
endIndex := startIndex + size
if endIndex > totalElements {
endIndex = totalElements
}
pageData := data[startIndex:endIndex]
return pageData, totalElements, totalPages
}
type workspaceMapper struct {
orgCache *cache.OrganizationCache
orgMapper *organizationMapper
groupCache *cache.GroupCache
}
func newWorkspaceMapper() *workspaceMapper {
return &workspaceMapper{
orgCache: cache.NewOrganizationCache(),
orgMapper: newOrganizationMapper(),
groupCache: cache.NewGroupCache(),
}
}
func (mp *workspaceMapper) mapOne(m model.Workspace, userID string) (*Workspace, error) {
org, err := mp.orgCache.Get(m.GetOrganizationID())
if err != nil {
return nil, err
}
v, err := mp.orgMapper.mapOne(org, userID)
if err != nil {
return nil, err
}
res := &Workspace{
ID: m.GetID(),
Name: m.GetName(),
RootID: m.GetRootID(),
StorageCapacity: m.GetStorageCapacity(),
Organization: *v,
CreateTime: m.GetCreateTime(),
UpdateTime: m.GetUpdateTime(),
}
res.Permission = ""
for _, p := range m.GetUserPermissions() {
if p.GetUserID() == userID && model.GetPermissionWeight(p.GetValue()) > model.GetPermissionWeight(res.Permission) {
res.Permission = p.GetValue()
}
}
for _, p := range m.GetGroupPermissions() {
g, err := mp.groupCache.Get(p.GetGroupID())
if err != nil {
return nil, err
}
for _, u := range g.GetUsers() {
if u == userID && model.GetPermissionWeight(p.GetValue()) > model.GetPermissionWeight(res.Permission) {
res.Permission = p.GetValue()
}
}
}
return res, nil
}
func (mp *workspaceMapper) mapMany(workspaces []model.Workspace, userID string) ([]*Workspace, error) {
res := make([]*Workspace, 0)
for _, f := range workspaces {
v, err := mp.mapOne(f, userID)
if err != nil {
return nil, err
}
res = append(res, v)
}
return res, nil
}

View File

@@ -0,0 +1 @@
subject: "Invitation to join a Voltaserve organization"

View File

@@ -0,0 +1,41 @@
<html>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap"
rel="stylesheet"
/>
<style>
.container {
font-family: "IBM Plex Sans", sans-serif;
font-size: 14px;
color: black;
width: 580px;
margin-left: auto;
margin-right: auto;
}
.link {
color: #0c4cf3 !important;
}
.link:visited {
color: #0c4cf3 !important;
}
</style>
</head>
<body>
<div class="container">
<p>Hello,</p>
<p>
You have been invited by <b>{{.USER_FULL_NAME}}</b> to join the
organization <b>{{.ORGANIZATION_NAME}}</b>. Please follow this link to
view your incoming invitations:
</p>
<p>
<a class="link" href="{{.UI_URL}}/account/invitation">
View incoming invitations
</a>
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,2 @@
You have been invited by {{.USER_FULL_NAME}} to join the organization {{.ORGANIZATION_NAME}}.
Please follow this link to sign up: {{.UI_URL}}/sign-up

View File

@@ -0,0 +1 @@
subject: "Invitation to join a Voltaserve organization"

View File

@@ -0,0 +1,39 @@
<html>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap"
rel="stylesheet"
/>
<style>
.container {
font-family: "IBM Plex Sans", sans-serif;
font-size: 14px;
color: black;
width: 580px;
margin-left: auto;
margin-right: auto;
}
.link {
color: #0c4cf3 !important;
}
.link:visited {
color: #0c4cf3 !important;
}
</style>
</head>
<body>
<div class="container">
<p>Hello,</p>
<p>
You have been invited by <b>{{.USER_FULL_NAME}}</b> to join the
organization <b>{{.ORGANIZATION_NAME}}</b> in Voltaserve. Please follow
this link to sign up:
</p>
<p>
<a class="link" href="{{.UI_URL}}/sign-up">Sign up</a>
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,2 @@
You have been invited by {{.USER_FULL_NAME}} to join the organization {{.ORGANIZATION_NAME}}.
Please follow this link to view your incoming invitations: {{.UI_URL}}/account/incoming-invitations