Compare commits

..

1 Commits

Author SHA1 Message Date
MaysWind 7a7892a403 backport commit(3d8ecb42c1) to 0.4.0 2023-09-24 22:35:22 +08:00
295 changed files with 11427 additions and 27220 deletions
-1
View File
@@ -31,7 +31,6 @@ jobs:
- name: Set up the environment
run: |
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
cat >> docker/custom-backend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- name: Set up the environment
run: |
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
sed -i 's#FROM #FROM ${{ secrets.DOCKER_REPO }}/mirrors/#g' Dockerfile
cat >> docker/custom-backend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
+4 -5
View File
@@ -1,5 +1,5 @@
# Build backend binary file
FROM golang:1.21.12-alpine3.20 AS be-builder
FROM golang:1.20.8-alpine3.17 AS be-builder
ARG RELEASE_BUILD
ENV RELEASE_BUILD=$RELEASE_BUILD
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
@@ -9,7 +9,7 @@ RUN apk add git gcc g++ libc-dev
RUN ./build.sh backend
# Build frontend files
FROM --platform=$BUILDPLATFORM node:18.20.3-alpine3.20 AS fe-builder
FROM node:18.17.1-alpine3.17 AS fe-builder
ARG RELEASE_BUILD
ENV RELEASE_BUILD=$RELEASE_BUILD
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
@@ -19,7 +19,7 @@ RUN apk add git
RUN ./build.sh frontend
# Package docker image
FROM alpine:3.20.1
FROM alpine:3.17.5
LABEL maintainer="MaysWind <i@mayswind.net>"
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
RUN apk --no-cache add tzdata
@@ -27,8 +27,7 @@ COPY docker/docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
RUN mkdir -p /ezbookkeeping && chown 1000:1000 /ezbookkeeping \
&& mkdir -p /ezbookkeeping/data && chown 1000:1000 /ezbookkeeping/data \
&& mkdir -p /ezbookkeeping/log && chown 1000:1000 /ezbookkeeping/log \
&& mkdir -p /ezbookkeeping/storage && chown 1000:1000 /ezbookkeeping/storage
&& mkdir -p /ezbookkeeping/log && chown 1000:1000 /ezbookkeeping/log
WORKDIR /ezbookkeeping
COPY --from=be-builder --chown=1000:1000 /go/src/github.com/mayswind/ezbookkeeping/ezbookkeeping /ezbookkeeping/ezbookkeeping
COPY --from=fe-builder --chown=1000:1000 /go/src/github.com/mayswind/ezbookkeeping/dist /ezbookkeeping/public
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2024 MaysWind (i@mayswind.net)
Copyright (c) 2020-2023 MaysWind (i@mayswind.net)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+1 -19
View File
@@ -8,8 +8,6 @@
## Introduction
ezBookkeeping is a lightweight personal bookkeeping app hosted by yourself. It can be deployed on almost all platforms, including Windows, macOS and Linux on x86, amd64 and ARM architectures. You can even deploy it on an raspberry device. It also supports many different databases, including sqlite and mysql. With docker, you can just deploy it via one command without complicated configuration.
Online Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-demo.mayswind.net)
## Features
1. Open source & Self-hosted
2. Lightweight & Fast
@@ -55,35 +53,19 @@ Latest Daily Build:
### Install from binary
Latest release: [https://github.com/mayswind/ezbookkeeping/releases](https://github.com/mayswind/ezbookkeeping/releases)
**Linux / macOS**
$ ./ezbookkeeping server run
**Windows**
> .\ezbookkeeping.exe server run
ezBookkeeping will listen at port 8080 as default. Then you can visit `http://{YOUR_HOST_ADDRESS}:8080/` .
ezBookkeeping will listen at port 8080 as default. Then you can visit http://{YOUR_HOST_ADDRESS}:8080/ .
### Build from source
Make sure you have [Golang](https://golang.org/), [GCC](http://gcc.gnu.org/), [Node.js](https://nodejs.org/) and [NPM](https://www.npmjs.com/) installed. Then download the source code, and follow these steps:
**Linux / macOS**
$ ./build.sh package -o ezbookkeeping.tar.gz
All the files will be packaged in `ezbookkeeping.tar.gz`.
**Windows**
> .\build.bat package -o ezbookkeeping.zip
All the files will be packaged in `ezbookkeeping.zip`.
You can also build docker image, make sure you have [docker](https://www.docker.com/) installed, then follow these steps:
**Linux**
$ ./build.sh docker
## Documents
-264
View File
@@ -1,264 +0,0 @@
@echo off
set "TYPE="
set "NO_LINT=0"
set "NO_TEST=0"
set "RELEASE=%RELEASE_BUILD%"
set "RELEASE_TYPE=unknown"
set "VERSION="
set "COMMIT_HASH="
set "BUILD_UNIXTIME="
set "BUILD_DATE="
set "PACKAGE_FILENAME="
for /f %%a in ('"prompt $E$S & echo on & for %%b in (1) do rem"') do set "ESC=%%a"
if "%~1"=="" call :show_help & goto :end
goto :pre_parse_args
:echo_red
echo %ESC%[91m%~1%ESC%[0m
goto :eof
:set_unixtime
setlocal enableextensions
for /f %%x in ('wmic path win32_utctime get /format:list ^| findstr "="') do set %%x
set /a z=(14-100%Month%%%100)/12, y=10000%Year%%%10000-z
set /a ut=y*365+y/4-y/100+y/400+(153*(100%Month%%%100+12*z-3)+2)/5+Day-719469
set /a ut=ut*86400+100%Hour%%%100*3600+100%Minute%%%100*60+100%Second%%%100
endlocal & set "%1=%ut%" & goto :eof
:set_date
setlocal enableextensions
for /f %%x in ('wmic path win32_localtime get /format:list ^| findstr "="') do set %%x
if %Month% lss 10 set "Month=0%Month%"
if %Day% lss 10 set "Day=0%Day%"
endlocal & set "%1=%Year%%Month%%Day%" & goto :eof
:check_dependency
if "%~1"=="" goto :eof
where /q %~1 || call :echo_red "Error: "%~1" is required." && goto :end
shift
goto :check_dependency
:show_help
echo ezBookkeeping build script for Windows
echo.
echo Usage:
echo build.cmd type [options]
echo.
echo Types:
echo backend Build backend binary file
echo frontend Build frontend files
echo package Build package archive
echo.
echo Options:
echo /r, --release Build release (The script will use environment variable "RELEASE_BUILD" to detect whether this is release building by default)
echo /o, --output ^<filename^> Package file name (For "package" type only)
echo --no-lint Do not execute lint check before building
echo --no-test Do not execute unit testing before building
echo /h, --help Show help
goto :eof
:pre_parse_args
if "%~1"=="" goto :post_parse_args
if /i "%~1"=="backend" set "TYPE=%~1" & shift
if /i "%~1"=="frontend" set "TYPE=%~1" & shift
if /i "%~1"=="package" set "TYPE=%~1" & shift
:parse_args
if "%~1"=="" goto :post_parse_args
if /i "%~1"=="/r" set "RELEASE=1" & shift & goto :parse_args
if /i "%~1"=="-r" set "RELEASE=1" & shift & goto :parse_args
if /i "%~1"=="--release" set "RELEASE=1" & shift & goto :parse_args
if /i "%~1"=="/o" set "PACKAGE_FILENAME=%~2" & shift & shift & goto :parse_args
if /i "%~1"=="-o" set "PACKAGE_FILENAME=%~2" & shift & shift & goto :parse_args
if /i "%~1"=="--output" set "PACKAGE_FILENAME=%~2" & shift & shift & goto :parse_args
if /i "%~1"=="--no-lint" set "NO_LINT=1" & shift & goto :parse_args
if /i "%~1"=="--no-test" set "NO_TEST=1" & shift & goto :parse_args
if /i "%~1"=="/h" call :show_help & goto :end
if /i "%~1"=="-h" call :show_help & goto :end
if /i "%~1"=="--help" call :show_help & goto :end
call :echo_red "Invalid argument: %~1" & call :show_help & goto :end
:post_parse_args
if "%RELEASE%"=="" set "RELEASE=0"
if "%RELEASE%"=="0" (
set "RELEASE_TYPE=snapshot"
) else (
set "RELEASE_TYPE=release"
)
:check_type_dependencies
if not defined TYPE call :echo_red "Error: No specified type" & call :show_help & goto :end
call :check_dependency "git"
if "%TYPE%"=="backend" call :check_dependency "go" "gcc"
if "%TYPE%"=="frontend" call :check_dependency "node" "npm"
if "%TYPE%"=="package" call :check_dependency "go" "gcc" "node" "npm" "7z"
if not "%errorlevel%"=="0" goto :end
:set_build_parameters
for /f "tokens=2 delims=:" %%x in ('findstr "\"version\": \"*\"," package.json') do set "VERSION=%%x"
set VERSION=%VERSION: =%
set VERSION=%VERSION:,=%
set VERSION=%VERSION:"=%
for /f %%x in ('git rev-parse --short HEAD') do set "COMMIT_HASH=%%x"
call :set_unixtime BUILD_UNIXTIME
call :set_date BUILD_DATE
:main
if "%TYPE%"=="backend" call :build_backend & goto :end
if "%TYPE%"=="frontend" call :build_frontend & goto :end
if "%TYPE%"=="package" call :build_package & goto :end
goto :end
:build_backend
setlocal enabledelayedexpansion
echo Pulling backend dependencies...
call go get .
if "%NO_LINT%"=="0" (
echo Executing backend lint checking...
call go vet -v .\...
if !errorlevel! neq 0 (
call :echo_red "Error: Failed to pass lint checking"
goto :end
)
)
if "%NO_TEST%"=="0" (
echo Executing backend unit testing...
call go clean -cache
call go test .\... -v
if !errorlevel! neq 0 (
call :echo_red "Error: Failed to pass unit testing"
goto :end
)
)
endlocal
set "CGO_ENABLED=1"
setlocal
set "backend_build_extra_arguments=-X main.Version=%VERSION%"
set "backend_build_extra_arguments=%backend_build_extra_arguments% -X main.CommitHash=%COMMIT_HASH%"
if "%RELEASE%"=="0" (
set "backend_build_extra_arguments=%backend_build_extra_arguments% -X main.BuildUnixTime=%BUILD_UNIXTIME%"
)
echo Building backend binary file (%RELEASE_TYPE%)...
call go build -a -v -trimpath -tags timetzdata -ldflags "-w -s -linkmode external -extldflags '-static' %backend_build_extra_arguments%" -o ezbookkeeping.exe ezbookkeeping.go
endlocal
set "CGO_ENABLED="
goto :eof
:build_frontend
setlocal enabledelayedexpansion
echo Pulling frontend dependencies...
call npm install
if "%NO_LINT%"=="0" (
echo Executing frontend lint checking...
call npm run lint
if !errorlevel! neq 0 (
call :echo_red "Error: Failed to pass lint checking"
goto :end
)
)
endlocal
echo Building frontend files(%RELEASE_TYPE%)...
if "%RELEASE%"=="0" (
set "buildUnixTime=%BUILD_UNIXTIME%"
call npm run build
set "buildUnixTime="
) else (
call npm run build
)
goto :eof
:build_package
setlocal enabledelayedexpansion
set "package_file_name=%VERSION%"
if "%RELEASE%"=="0" (
set "build_date="
set "package_file_name=%package_file_name%-%build_date%"
)
set "package_file_name=ezbookkeeping-%package_file_name%-windows.zip"
if defined PACKAGE_FILENAME set "package_file_name=%PACKAGE_FILENAME%"
echo Building package archive "%package_file_name%" (%RELEASE_TYPE%)...
call :build_backend
if !errorlevel! neq 0 (
goto :end
)
call :build_frontend
if !errorlevel! neq 0 (
goto :end
)
rmdir package /s /q
mkdir package
mkdir package\data
mkdir package\storage
mkdir package\log
xcopy ezbookkeeping.exe package\
xcopy dist package\public /e /i
xcopy conf package\conf /e /i
xcopy templates package\templates /e /i
xcopy LICENSE package\
cd package
if !errorlevel! neq 0 (
call :echo_red "Error: Build Failed"
goto :end
)
call 7z a -r -tzip -mx9 ..\%package_file_name% package *
cd ..
endlocal
goto :eof
:end
set "TYPE="
set "NO_LINT="
set "NO_TEST="
set "RELEASE="
set "RELEASE_TYPE="
set "VERSION="
set "COMMIT_HASH="
set "BUILD_UNIXTIME="
set "BUILD_DATE="
set "PACKAGE_FILENAME="
exit /B
-2
View File
@@ -202,12 +202,10 @@ build_package() {
rm -rf package
mkdir package
mkdir package/data
mkdir package/storage
mkdir package/log
cp ezbookkeeping package/
cp -R dist package/public
cp -R conf package/conf
cp -R templates package/templates
cp LICENSE package/
cd package || { echo_red "Error: Build Failed"; exit 1; }
+2 -10
View File
@@ -58,7 +58,7 @@ func updateAllDatabaseTablesStructure() error {
return err
}
log.BootInfof("[database.updateAllDatabaseTablesStructure] two-factor table maintained successfully")
log.BootInfof("[database.updateAllDatabaseTablesStructure] two factor table maintained successfully")
err = datastore.Container.UserStore.SyncStructs(new(models.TwoFactorRecoveryCode))
@@ -66,7 +66,7 @@ func updateAllDatabaseTablesStructure() error {
return err
}
log.BootInfof("[database.updateAllDatabaseTablesStructure] two-factor recovery code table maintained successfully")
log.BootInfof("[database.updateAllDatabaseTablesStructure] two factor recovery code table maintained successfully")
err = datastore.Container.TokenStore.SyncStructs(new(models.TokenRecord))
@@ -116,13 +116,5 @@ func updateAllDatabaseTablesStructure() error {
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction tag index table maintained successfully")
err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionTemplate))
if err != nil {
return err
}
log.BootInfof("[database.updateAllDatabaseTablesStructure] transaction template table maintained successfully")
return nil
}
-26
View File
@@ -7,12 +7,10 @@ import (
"github.com/urfave/cli/v2"
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
"github.com/mayswind/ezbookkeeping/pkg/exchangerates"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/mail"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/storage"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/uuid"
)
@@ -57,10 +55,6 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
return nil, err
}
if config.SecretKeyNoSet {
log.BootWarnf("[initializer.initializeSystem] \"secret_key\" in config file is not set, please change it to keep your user data safe")
}
settings.SetCurrentConfig(config)
err = datastore.InitializeDataStore(config)
@@ -81,15 +75,6 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
return nil, err
}
err = storage.InitializeStorageContainer(config)
if err != nil {
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] initializes object storage failed, because %s", err.Error())
}
return nil, err
}
err = uuid.InitializeUuidGenerator(config)
if err != nil {
@@ -99,15 +84,6 @@ func initializeSystem(c *cli.Context) (*settings.Config, error) {
return nil, err
}
err = duplicatechecker.InitializeDuplicateChecker(config)
if err != nil {
if !isDisableBootLog {
log.BootErrorf("[initializer.initializeSystem] initializes duplicate checker failed, because %s", err.Error())
}
return nil, err
}
err = mail.InitializeMailer(config)
if err != nil {
@@ -145,9 +121,7 @@ func getConfigWithoutSensitiveData(config *settings.Config) *settings.Config {
clonedConfig.DatabaseConfig.DatabasePassword = "****"
clonedConfig.SMTPConfig.SMTPPasswd = "****"
clonedConfig.MinIOConfig.SecretAccessKey = "****"
clonedConfig.SecretKey = "****"
clonedConfig.AmapApplicationSecret = "****"
return clonedConfig
}
+6 -62
View File
@@ -2,14 +2,13 @@ package cmd
import (
"fmt"
"github.com/mayswind/ezbookkeeping/pkg/models"
"os"
"github.com/urfave/cli/v2"
clis "github.com/mayswind/ezbookkeeping/pkg/cli"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
@@ -230,22 +229,9 @@ var UserData = &cli.Command{
},
},
},
{
Name: "transaction-tag-index-fix-transaction-time",
Usage: "Fix the transaction tag index data which does not have transaction time",
Action: fixTransactionTagIndexNotHaveTransactionTime,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"n"},
Required: true,
Usage: "Specific user name",
},
},
},
{
Name: "transaction-export",
Usage: "Export user all transactions to file",
Usage: "Export user all transactions to csv file",
Action: exportUserTransaction,
Flags: []cli.Flag{
&cli.StringFlag{
@@ -260,12 +246,6 @@ var UserData = &cli.Command{
Required: true,
Usage: "Specific exported file path (e.g. transaction.csv)",
},
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Required: false,
Usage: "Export file type, support csv or tsv, default is csv",
},
},
},
},
@@ -488,11 +468,11 @@ func disableUser2FA(c *cli.Context) error {
err = clis.UserData.DisableUserTwoFactorAuthorization(c, username)
if err != nil {
log.BootErrorf("[user_data.disableUser2FA] error occurs when disabling user two-factor authorization")
log.BootErrorf("[user_data.disableUser2FA] error occurs when disabling user two factor authorization")
return err
}
log.BootInfof("[user_data.disableUser2FA] two-factor authorization of user \"%s\" has been disabled", username)
log.BootInfof("[user_data.disableUser2FA] two factor authorization of user \"%s\" has been disabled", username)
return nil
}
@@ -566,29 +546,6 @@ func checkUserTransactionAndAccount(c *cli.Context) error {
return nil
}
func fixTransactionTagIndexNotHaveTransactionTime(c *cli.Context) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
username := c.String("username")
log.BootInfof("[user_data.fixTransactionTagIndexNotHaveTransactionTime] starting fixing user \"%s\" transaction tag index data", username)
_, err = clis.UserData.FixTransactionTagIndexWithTransactionTime(c, username)
if err != nil {
log.BootErrorf("[user_data.fixTransactionTagIndexNotHaveTransactionTime] error occurs when fixing user data")
return err
}
log.BootInfof("[user_data.fixTransactionTagIndexNotHaveTransactionTime] user transaction tag index data has been fixed successfully")
return nil
}
func exportUserTransaction(c *cli.Context) error {
_, err := initializeSystem(c)
@@ -598,15 +555,9 @@ func exportUserTransaction(c *cli.Context) error {
username := c.String("username")
filePath := c.String("file")
fileType := c.String("type")
if fileType != "" && fileType != "csv" && fileType != "tsv" {
log.BootErrorf("[user_data.exportUserTransaction] export file type is not supported")
return errs.ErrNotSupported
}
if filePath == "" {
log.BootErrorf("[user_data.exportUserTransaction] export file path is unspecified")
log.BootErrorf("[user_data.exportUserTransaction] export file path is not specified")
return os.ErrNotExist
}
@@ -619,7 +570,7 @@ func exportUserTransaction(c *cli.Context) error {
log.BootInfof("[user_data.exportUserTransaction] starting exporting user \"%s\" data", username)
content, err := clis.UserData.ExportTransaction(c, username, fileType)
content, err := clis.UserData.ExportTransaction(c, username)
if err != nil {
log.BootErrorf("[user_data.exportUserTransaction] error occurs when exporting user data")
@@ -654,12 +605,6 @@ func printUserInfo(user *models.User) {
fmt.Printf("[ShortDateFormat] %s (%d)\n", user.ShortDateFormat, user.ShortDateFormat)
fmt.Printf("[LongTimeFormat] %s (%d)\n", user.LongTimeFormat, user.LongTimeFormat)
fmt.Printf("[ShortTimeFormat] %s (%d)\n", user.ShortTimeFormat, user.ShortTimeFormat)
fmt.Printf("[DecimalSeparator] %s (%d)\n", user.DecimalSeparator, user.DecimalSeparator)
fmt.Printf("[DigitGroupingSymbol] %s (%d)\n", user.DigitGroupingSymbol, user.DigitGroupingSymbol)
fmt.Printf("[DigitGrouping] %s (%d)\n", user.DigitGrouping, user.DigitGrouping)
fmt.Printf("[CurrencyDisplayType] %s (%d)\n", user.CurrencyDisplayType, user.CurrencyDisplayType)
fmt.Printf("[ExpenseAmountColor] %s (%d)\n", user.ExpenseAmountColor, user.ExpenseAmountColor)
fmt.Printf("[IncomeAmountColor] %s (%d)\n", user.IncomeAmountColor, user.IncomeAmountColor)
fmt.Printf("[Deleted] %t\n", user.Deleted)
fmt.Printf("[EmailVerified] %t\n", user.EmailVerified)
fmt.Printf("[CreatedAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(user.CreatedUnixTime), user.CreatedUnixTime)
@@ -680,6 +625,5 @@ func printUserInfo(user *models.User) {
func printTokenInfo(token *models.TokenRecord) {
fmt.Printf("[CreatedAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(token.CreatedUnixTime), token.CreatedUnixTime)
fmt.Printf("[ExpiredAt] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(token.ExpiredUnixTime), token.ExpiredUnixTime)
fmt.Printf("[LastSeen] %s (%d)\n", utils.FormatUnixTimeToLongDateTimeInServerTimezone(token.LastSeenUnixTime), token.LastSeenUnixTime)
fmt.Printf("[UserAgent] %s\n", token.UserAgent)
}
+2 -3
View File
@@ -66,7 +66,7 @@ func parseRequestId(c *cli.Context) error {
return err
}
newRequestId := defaultGenerator.GenerateRequestId(net.IPv4zero.String(), 0)
newRequestId := defaultGenerator.GenerateRequestId(net.IPv4zero.String())
newRequestIdInfo, err := defaultGenerator.ParseRequestIdInfo(newRequestId)
printRequestIdInfo(requestId, requestIdInfo, newRequestIdInfo)
@@ -114,6 +114,7 @@ func printRequestIdInfo(requestId string, requestIdInfo *requestid.RequestIdInfo
fmt.Printf("[SecondsElapsedToday] %d\n", requestIdInfo.SecondsElapsedToday)
}
fmt.Printf("[RandomNumber] %d\n", requestIdInfo.RandomNumber)
fmt.Printf("[RequestSeqId] %d\n", requestIdInfo.RequestSeqId)
fmt.Printf("[IsClientIpv6] %t\n", requestIdInfo.IsClientIpv6)
@@ -124,6 +125,4 @@ func printRequestIdInfo(requestId string, requestIdInfo *requestid.RequestIdInfo
binary.BigEndian.PutUint32(ip, requestIdInfo.ClientIp)
fmt.Printf("[ClientIpv4] %s\n", ip.String())
}
fmt.Printf("[ClientPort] %d\n", requestIdInfo.ClientPort)
}
+16 -74
View File
@@ -44,13 +44,13 @@ func startWebServer(c *cli.Context) error {
return err
}
log.BootInfof("[webserver.startWebServer] static root path is %s", config.StaticRootPath)
log.BootInfof("[server.startWebServer] static root path is %s", config.StaticRootPath)
if config.AutoUpdateDatabase {
err = updateAllDatabaseTablesStructure()
if err != nil {
log.BootErrorf("[webserver.startWebServer] update database table structure failed, because %s", err.Error())
log.BootErrorf("[server.startWebServer] update database table structure failed, because %s", err.Error())
return err
}
}
@@ -58,7 +58,7 @@ func startWebServer(c *cli.Context) error {
err = requestid.InitializeRequestIdGenerator(config)
if err != nil {
log.BootErrorf("[webserver.startWebServer] initializes requestid generator failed, because %s", err.Error())
log.BootErrorf("[server.startWebServer] initializes requestid generator failed, because %s", err.Error())
return err
}
@@ -68,7 +68,7 @@ func startWebServer(c *cli.Context) error {
uuidServerInfo = fmt.Sprintf(", current uuid server id is %d", config.UuidServerId)
}
log.BootInfof("[webserver.startWebServer] %s%s", serverInfo, uuidServerInfo)
log.BootInfof("[server.startWebServer] %s%s", serverInfo, uuidServerInfo)
if config.Mode == settings.MODE_PRODUCTION {
gin.SetMode(gin.ReleaseMode)
@@ -89,7 +89,6 @@ func startWebServer(c *cli.Context) error {
_ = v.RegisterValidation("validEmail", validators.ValidEmail)
_ = v.RegisterValidation("validCurrency", validators.ValidCurrency)
_ = v.RegisterValidation("validHexRGBColor", validators.ValidHexRGBColor)
_ = v.RegisterValidation("validAmountFilter", validators.ValidAmountFilter)
}
router.NoRoute(bindApi(api.Default.ApiNotFound))
@@ -145,14 +144,6 @@ func startWebServer(c *cli.Context) error {
router.StaticFile("/desktop/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
}
if config.AvatarProvider == settings.InternalAvatarProvider {
avatarRoute := router.Group("/avatar")
avatarRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
{
avatarRoute.GET("/:fileName", bindImage(api.Users.UserGetAvatarHandler))
}
}
router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler))
if config.Mode == settings.MODE_DEVELOPMENT {
@@ -169,17 +160,9 @@ func startWebServer(c *cli.Context) error {
config.MapProvider == settings.OpenTopoMapProvider ||
config.MapProvider == settings.OPNVKarteMapProvider ||
config.MapProvider == settings.CyclOSMMapProvider ||
config.MapProvider == settings.CartoDBMapProvider ||
config.MapProvider == settings.TomTomMapProvider ||
config.MapProvider == settings.TianDiTuProvider ||
config.MapProvider == settings.CustomProvider {
config.MapProvider == settings.TomTomMapProvider {
proxyRoute.GET("/map/tile/:zoomLevel/:coordinateX/:fileName", bindProxy(api.MapImages.MapTileImageProxyHandler))
}
if config.MapProvider == settings.TianDiTuProvider ||
(config.MapProvider == settings.CustomProvider && config.CustomMapTileServerAnnotationLayerUrl != "") {
proxyRoute.GET("/map/annotation/:zoomLevel/:coordinateX/:fileName", bindProxy(api.MapImages.MapAnnotationImageProxyHandler))
}
}
}
@@ -195,7 +178,7 @@ func startWebServer(c *cli.Context) error {
qrCodeRoute.Use(bindMiddleware(middlewares.RequestId(config)))
{
qrCodeCacheStore := persistence.NewInMemoryStore(time.Minute)
qrCodeRoute.GET("/mobile_url.png", bindCachedImage(api.QrCodes.MobileUrlQrCodeHandler, qrCodeCacheStore))
qrCodeRoute.GET("/mobile_url.png", bindCachedPngImage(api.QrCodes.MobileUrlQrCodeHandler, qrCodeCacheStore))
}
apiRoute := router.Group("/api")
@@ -253,16 +236,11 @@ func startWebServer(c *cli.Context) error {
apiV1Route.GET("/users/profile/get.json", bindApi(api.Users.UserProfileHandler))
apiV1Route.POST("/users/profile/update.json", bindApiWithTokenUpdate(api.Users.UserUpdateProfileHandler, config))
if config.AvatarProvider == settings.InternalAvatarProvider {
apiV1Route.POST("/users/avatar/update.json", bindApi(api.Users.UserUpdateAvatarHandler))
apiV1Route.POST("/users/avatar/remove.json", bindApi(api.Users.UserRemoveAvatarHandler))
}
if config.EnableUserVerifyEmail {
apiV1Route.POST("/users/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByLoginedUserHandler))
}
// Two-Factor Authorization
// Two Factor Authorization
if config.EnableTwoFactor {
apiV1Route.GET("/users/2fa/status.json", bindApi(api.TwoFactorAuthorizations.TwoFactorStatusHandler))
apiV1Route.POST("/users/2fa/enable/request.json", bindApi(api.TwoFactorAuthorizations.TwoFactorEnableRequestHandler))
@@ -276,8 +254,7 @@ func startWebServer(c *cli.Context) error {
apiV1Route.POST("/data/clear.json", bindApi(api.DataManagements.ClearDataHandler))
if config.EnableDataExport {
apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataToEzbookkeepingCSVHandler))
apiV1Route.GET("/data/export.tsv", bindTsv(api.DataManagements.ExportDataToEzbookkeepingTSVHandler))
apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataHandler))
}
// Accounts
@@ -294,8 +271,8 @@ func startWebServer(c *cli.Context) error {
apiV1Route.GET("/transactions/list.json", bindApi(api.Transactions.TransactionListHandler))
apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionMonthListHandler))
apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler))
apiV1Route.GET("/transactions/statistics/trends.json", bindApi(api.Transactions.TransactionStatisticsTrendsHandler))
apiV1Route.GET("/transactions/amounts.json", bindApi(api.Transactions.TransactionAmountsHandler))
apiV1Route.GET("/transactions/amounts/by_month.json", bindApi(api.Transactions.TransactionMonthAmountsHandler))
apiV1Route.GET("/transactions/get.json", bindApi(api.Transactions.TransactionGetHandler))
apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler))
apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler))
@@ -320,15 +297,6 @@ func startWebServer(c *cli.Context) error {
apiV1Route.POST("/transaction/tags/move.json", bindApi(api.TransactionTags.TagMoveHandler))
apiV1Route.POST("/transaction/tags/delete.json", bindApi(api.TransactionTags.TagDeleteHandler))
// Transaction Templates
apiV1Route.GET("/transaction/templates/list.json", bindApi(api.TransactionTemplates.TemplateListHandler))
apiV1Route.GET("/transaction/templates/get.json", bindApi(api.TransactionTemplates.TemplateGetHandler))
apiV1Route.POST("/transaction/templates/add.json", bindApi(api.TransactionTemplates.TemplateCreateHandler))
apiV1Route.POST("/transaction/templates/modify.json", bindApi(api.TransactionTemplates.TemplateModifyHandler))
apiV1Route.POST("/transaction/templates/hide.json", bindApi(api.TransactionTemplates.TemplateHideHandler))
apiV1Route.POST("/transaction/templates/move.json", bindApi(api.TransactionTemplates.TemplateMoveHandler))
apiV1Route.POST("/transaction/templates/delete.json", bindApi(api.TransactionTemplates.TemplateDeleteHandler))
// Exchange Rates
apiV1Route.GET("/exchange_rates/latest.json", bindApi(api.ExchangeRates.LatestExchangeRateHandler))
}
@@ -337,20 +305,20 @@ func startWebServer(c *cli.Context) error {
listenAddr := fmt.Sprintf("%s:%d", config.HttpAddr, config.HttpPort)
if config.Protocol == settings.SCHEME_SOCKET {
log.BootInfof("[webserver.startWebServer] will run at socks:%s", config.UnixSocketPath)
log.BootInfof("[server.startWebServer] will run at socks:%s", config.UnixSocketPath)
err = router.RunUnix(config.UnixSocketPath)
} else if config.Protocol == settings.SCHEME_HTTP {
log.BootInfof("[webserver.startWebServer] will run at http://%s", listenAddr)
log.BootInfof("[server.startWebServer] will run at http://%s", listenAddr)
err = router.Run(listenAddr)
} else if config.Protocol == settings.SCHEME_HTTPS {
log.BootInfof("[webserver.startWebServer] will run at https://%s", listenAddr)
log.BootInfof("[server.startWebServer] will run at https://%s", listenAddr)
err = router.RunTLS(listenAddr, config.CertFile, config.CertKeyFile)
} else {
err = errs.ErrInvalidProtocol
}
if err != nil {
log.BootErrorf("[webserver.startWebServer] cannot start, because %s", err)
log.BootErrorf("[server.startWebServer] cannot start, because %s", err)
return err
}
@@ -406,41 +374,15 @@ func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc {
}
}
func bindTsv(fn core.DataHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
result, fileName, err := fn(c)
if err != nil {
utils.PrintDataErrorResult(c, "text/text", err)
} else {
utils.PrintDataSuccessResult(c, "text/tab-separated-values", fileName, result)
}
}
}
func bindImage(fn core.ImageHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
result, contentType, err := fn(c)
if err != nil {
utils.PrintDataErrorResult(c, "text/text", err)
} else {
utils.PrintDataSuccessResult(c, contentType, "", result)
}
}
}
func bindCachedImage(fn core.ImageHandlerFunc, store persistence.CacheStore) gin.HandlerFunc {
func bindCachedPngImage(fn core.DataHandlerFunc, store persistence.CacheStore) gin.HandlerFunc {
return cache.CachePage(store, time.Minute, func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
result, contentType, err := fn(c)
result, _, err := fn(c)
if err != nil {
utils.PrintDataErrorResult(c, "text/text", err)
} else {
utils.PrintDataSuccessResult(c, contentType, "", result)
utils.PrintDataSuccessResult(c, "img/png", "", result)
}
})
}
+21 -126
View File
@@ -25,7 +25,7 @@ root_url = %(protocol)s://%(domain)s:%(http_port)s/
cert_file =
cert_key_file =
# Unix socket path, for "socket" protocol only
# Unix socket path, for "socket" only
unix_socket =
# Static file root path (relative or absolute path)
@@ -47,10 +47,10 @@ name = ezbookkeeping
user = root
passwd =
# For "postgres" database only, Either "disable", "require" or "verify-full"
# For "postgres" only, Either "disable", "require" or "verify-full"
ssl_mode = disable
# For "sqlite3" database only, database file path (relative or absolute path)
# For "sqlite3" only, db file path (relative or absolute path)
db_path = data/ezbookkeeping.db
# Max idle connection number (0 - 65535, 0 means no idle connections are retained), default is 2
@@ -89,88 +89,33 @@ mode = console file
# Either "debug", "info", "warn", "error", default is "info"
level = info
# For "file" mode only, log file path (relative or absolute path)
# For "file" only, log file path (relative or absolute path)
log_path = log/ezbookkeeping.log
# For "file" only, request log file path (relative or absolute path). Leave blank if you want to write request log in default log file
request_log_path =
# For "file" only, query log file path (relative or absolute path). Leave blank if you want to write query log in default log file
query_log_path =
# For "file" only, whether rotate the log files
log_file_rotate = false
# For "file" only, maximum size (1 - 4294967295 bytes) of the log file before it gets rotated
log_file_max_size = 104857600
# For "file" only, maximum number of days to retain old log files. Set to 0 to retain all logs
log_file_max_days = 7
[storage]
# Object storage type, supports "local_filesystem" and "minio" currently
type = local_filesystem
# For "local_filesystem" storage only, the storage root path (relative or absolute path)
local_filesystem_path = storage/
# For "minio" storage only, the minio connection configuration
minio_endpoint = 127.0.0.1:9000
minio_location =
minio_access_key_id =
minio_secret_access_key =
# For "minio" storage only, whether enable ssl for minio connection
minio_use_ssl = false
# For "minio" storage only, set to true to skip tls verification when connect minio
minio_skip_tls_verify = false
# For "minio" storage only, the minio bucket
minio_bucket = ezbookkeeping
# For "minio" storage only, the root path to store files in minio
minio_root_path = /
[uuid]
# Uuid generator type, supports "internal" currently
generator_type = internal
# For "internal" uuid generator only, each server must have unique id (0 - 255)
# For "internal" only, each server must have unique id (0 - 255)
server_id = 0
[duplicate_checker]
# Duplicate checker type, supports "in_memory" currently
checker_type = in_memory
# For "in_memory" duplicate checker only, cleanup expired data interval seconds (1 - 4294967295), default is 60 (1 minutes)
cleanup_interval = 60
# The minimum interval seconds (0 - 4294967295) between duplicate submissions on the same page (exiting and re-entering the page is considered as a new session)
# Set to 0 to disable duplicate checker for new data submissions, default is 300 (5 minutes)
duplicate_submissions_interval = 300
[security]
# Used for signing, you must change it to keep your user data safe before you first run ezBookkeeping
secret_key =
# Set to true to enable two-factor authorization
# Set to true to enable two factor authorization
enable_two_factor = true
# Token expired seconds (60 - 4294967295), default is 2592000 (30 days)
# Token expired seconds (0 - 4294967295), default is 2592000 (30 days)
token_expired_time = 2592000
# Token minimum refresh interval (0 - 4294967295), the value should be less than token expired time
# Set to 0 to refresh the token every time when refreshing the front end, default is 86400 (1 day)
token_min_refresh_interval = 86400
# Temporary token expired seconds (60 - 4294967295), default is 300 (5 minutes)
# Temporary token expired seconds (0 - 4294967295), default is 300 (5 minutes)
temporary_token_expired_time = 300
# Email verify token expired seconds (60 - 4294967295), default is 3600 (60 minutes)
# Email verify token expired seconds (0 - 4294967295), default is 3600 (60 minutes)
email_verify_token_expired_time = 3600
# Password reset token expired seconds (60 - 4294967295), default is 3600 (60 minutes)
# Password reset token expired seconds (0 - 4294967295), default is 3600 (60 minutes)
password_reset_token_expired_time = 3600
# Add X-Request-Id header to response to track user request or error, default is true
@@ -193,36 +138,14 @@ enable_forget_password = true
forget_password_require_email_verify = false
# User avatar provider, supports the following types:
# "internal": Use the internal object storage to store user avatar (refer to "storage" settings), supports updating avatar by user self
# "gravatar": https://gravatar.com
# Leave blank if you want to disable user avatar
avatar_provider = internal
avatar_provider =
[data]
# Set to true to allow users to export their data
enable_export = true
[notification]
# Set to true to display custom notification in home page every time users register
enable_notification_after_register = false
# The notification content displayed each time users register, it supports multi-language configuration
# Add an underscore and a language tag after the setting key to configure the notification content in that language, the same below
# For example, after_login_notification_content_zh_hans means the notification content in Simplified Chinese
after_register_notification_content =
# Set to true to display custom notification in home page every time users login
enable_notification_after_login = false
# The notification content displayed each time users log in, it supports multi-language configuration
after_login_notification_content =
# Set to true to display custom notification in home page every time users open the app
enable_notification_after_open = false
# The notification content displayed each time users open the app, it supports multi-language configuration
after_open_notification_content =
[map]
# Map provider, supports the following types:
# "openstreetmap": https://www.openstreetmap.org
@@ -230,65 +153,41 @@ after_open_notification_content =
# "opentopomap": https://opentopomap.org
# "opnvkarte": https://publictransportmap.org
# "cyclosm": https://www.cyclosm.org
# "cartodb": https://carto.com/basemaps
# "tomtom": https://www.tomtom.com
# "tianditu": https://www.tianditu.gov.cn
# "googlemap": https://map.google.com
# "baidumap": https://map.baidu.com
# "amap": https://amap.com
# "custom": custom map tile server url
# Leave blank if you want to disable map
map_provider = openstreetmap
# Set to true to use the ezbookkeeping server to forward map data requests, for "openstreetmap", "openstreetmap_humanitarian", "opentopomap", "opnvkarte", "cyclosm", "cartodb", "tomtom", "tianditu" or "custom"
# Set to true to use the ezbookkeeping server to proxy map data requests, for "openstreetmap", "openstreetmap_humanitarian", "opentopomap", "opnvkarte", "cyclosm" or "tomtom"
map_data_fetch_proxy = false
# Proxy for ezbookkeeping server requesting original map data when map_data_fetch_proxy is set to true, supports "system" (use system proxy), "none" (do not use proxy), or proxy URL which starts with "http://", "https://" or "socks5://", default is "system"
proxy = system
# For "tomtom" map provider only, TomTom map API key, please visit https://developer.tomtom.com/how-to-get-tomtom-api-key for more information
# For "tomtom" only, TomTom map API key, please visit https://developer.tomtom.com/how-to-get-tomtom-api-key
tomtom_map_api_key =
# For "tianditu" map provider only, TianDiTu map application key, please visit https://console.tianditu.gov.cn/api/register for more information
tianditu_map_app_key =
# For "googlemap" map provider only, Google map JavaScript API key, please visit https://developers.google.com/maps/get-started for more information
# For "googlemap" only, Google map JavaScript API key, please visit https://developers.google.com/maps/get-started for more information
google_map_api_key =
# For "baidumap" map provider only, Baidu map JavaScript API application key, please visit https://lbsyun.baidu.com/index.php?title=jspopular3.0/guide/getkey for more information
# For "baidumap" only, Baidu map JavaScript API application key, please visit https://lbsyun.baidu.com/index.php?title=jspopular3.0/guide/getkey for more information
baidu_map_ak =
# For "amap" map provider only, Amap JavaScript API application key, please visit https://lbs.amap.com/api/javascript-api/guide/abc/prepare for more information
# For "amap" only, Amap JavaScript API application key, please visit https://lbs.amap.com/api/javascript-api/guide/abc/prepare for more information
amap_application_key =
# For "amap" map provider only, Amap JavaScript API security verification method, supports the following methods:
# For "amap" only, Amap JavaScript API security verification method, supports the following methods:
# "internal_proxy": use the internal proxy to request amap api with amap application secret (default)
# "external_proxy": use an external proxy to request amap api (amap application secret should be set by external proxy)
# "plain_text": append amap application secret to frontend request directly (insecurity for public network)
# Please visit https://developer.amap.com/api/jsapi-v2/guide/abc/load for more information
amap_security_verification_method = plain_text
# For "amap" map provider only, Amap JavaScript API application secret, this setting must be provided when "amap_security_verification_method" is set to "internal_proxy" or "plain_text", please visit https://lbs.amap.com/api/javascript-api/guide/abc/prepare for more information
# For "amap" only, Amap JavaScript API application secret, this setting must be provided when "amap_security_verification_method" is set to "internal_proxy" or "plain_text", please visit https://lbs.amap.com/api/javascript-api/guide/abc/prepare for more information
amap_application_secret =
# For "amap" map provider only, Amap JavaScript API external proxy url, this setting must be provided when "amap_security_verification_method" is set to "external_proxy"
# For "amap" only, Amap JavaScript API external proxy url, this setting must be provided when "amap_security_verification_method" is set to "external_proxy"
amap_api_external_proxy_url =
# For "custom" map provider only, the tile layer url of custom map tile server, supports {x}, {y} (coordinates) and {z} (zoom level) placeholders, like "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
custom_map_tile_server_url =
# For "custom" map provider only, the optional annotation layer url of custom map tile server, supports {x}, {y} (coordinates) and {z} (zoom level) placeholders
custom_map_tile_server_annotation_url =
# For "custom" map provider only, the min zoom level (0 - 255) for custom map tile server, default is 1
custom_map_tile_server_min_zoom_level = 1
# For "custom" map provider only, the max zoom level (0 - 255) for custom map tile server, default is 18
custom_map_tile_server_max_zoom_level = 18
# For "custom" map provider only, the default zoom level (0 - 255) for custom map tile server, default is 14
custom_map_tile_server_default_zoom_level = 14
[exchange_rates]
# Exchange rates data source, supports the following types:
# "euro_central_bank"
@@ -299,12 +198,8 @@ custom_map_tile_server_default_zoom_level = 14
# "monetary_authority_of_singapore"
data_source = euro_central_bank
# Requesting exchange rates data timeout (0 - 4294967295 milliseconds)
# Set to 0 to disable timeout for requesting exchange rates data, default is 10000 (10 seconds)
# Requesting exchange rates data timeout (0 - 4294967295 milliseconds), default is 10000 (10 seconds)
request_timeout = 10000
# Proxy for ezbookkeeping server requesting exchange rates data, supports "system" (use system proxy), "none" (do not use proxy), or proxy URL which starts with "http://", "https://" or "socks5://", default is "system"
proxy = system
# Set to true to skip tls verification when request exchange rates data
# Set to true skip tls verification when request exchange rates data
skip_tls_verify = false
+29 -42
View File
@@ -1,78 +1,65 @@
module github.com/mayswind/ezbookkeeping
go 1.21
go 1.20
require (
github.com/boombuler/barcode v1.0.2
github.com/gin-contrib/cache v1.3.0
github.com/gin-contrib/gzip v1.0.1
github.com/gin-gonic/gin v1.10.0
github.com/go-playground/validator/v10 v10.22.0
github.com/go-sql-driver/mysql v1.8.1
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/boombuler/barcode v1.0.1
github.com/gin-contrib/cache v1.2.0
github.com/gin-contrib/gzip v0.0.6
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/validator/v10 v10.15.1
github.com/go-sql-driver/mysql v1.7.1
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.22
github.com/minio/minio-go/v7 v7.0.74
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/mattn/go-sqlite3 v1.14.16
github.com/pquerna/otp v1.4.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v2 v2.27.1
github.com/stretchr/testify v1.8.4
github.com/urfave/cli/v2 v2.25.7
github.com/wk8/go-ordered-map/v2 v2.1.8
golang.org/x/crypto v0.25.0
golang.org/x/crypto v0.12.0
gopkg.in/ini.v1 v1.67.0
gopkg.in/mail.v2 v2.3.1
xorm.io/builder v0.3.13
xorm.io/xorm v1.3.9
xorm.io/xorm v1.3.2
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomodule/redigo v1.8.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/memcachier/mc/v3 v3.0.3 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
xorm.io/builder v0.3.12 // indirect
)
+683 -87
View File
@@ -1,182 +1,778 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw=
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/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/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
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/gin-contrib/cache v1.3.0 h1:wEEw38uvb4rTraQJVpd9ex4ZotXNlc0fSaSUsuPXS/w=
github.com/gin-contrib/cache v1.3.0/go.mod h1:EA63LrWGI5vwSI95TS5fgBrtxZ1tM2NKx+NrEeyEDcU=
github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/cache v1.2.0 h1:WA+AJR4kmHDTaLLShCHo/IeWVmmGRZ3Lsr3JQ46tFlE=
github.com/gin-contrib/cache v1.2.0/go.mod h1:2KkFL8PSnPF3Tt5E2Jpc3HWuBAUKqGZnClCFMm0tXQI=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
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.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
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.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
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/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
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/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE=
github.com/jackc/pgtype v1.8.0/go.mod h1:PqDKcEBtllAtk/2p6z6SHdXW5UB+MhE75tUol2OKexE=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc=
github.com/jackc/pgx/v4 v4.12.0/go.mod h1:fE547h6VulLPA3kySjfnSG/e2D861g/50JlVUa/ub60=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
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.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
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/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4=
github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug=
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.74 h1:fTo/XlPBTSpo3BAMshlwKL5RspXRv9us5UeHEGYCFe0=
github.com/minio/minio-go/v7 v7.0.74/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201126233918-771906719818/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
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/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
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/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U=
modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
modernc.org/ccgo/v3 v3.12.65/go.mod h1:D6hQtKxPNZiY6wDBtehSGKFKmyXn53F8nGTpH+POmS4=
modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8=
modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
modernc.org/libc v1.11.70/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
modernc.org/libc v1.11.87 h1:PzIzOqtlzMDDcCzJ5cUP6h/Ku6Fa9iyflP2ccTY64aE=
modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE=
modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8=
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY=
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU=
xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM=
xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM=
xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw=
+2862 -2655
View File
File diff suppressed because it is too large Load Diff
+27 -26
View File
@@ -1,6 +1,6 @@
{
"name": "ezbookkeeping",
"version": "0.5.0",
"version": "0.4.0",
"private": true,
"repository": {
"type": "git",
@@ -18,46 +18,47 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"dependencies": {
"@mdi/js": "^7.4.47",
"@vuepic/vue-datepicker": "^8.8.1",
"axios": "^1.7.2",
"@mdi/js": "^7.2.96",
"@vuepic/vue-datepicker": "^5.4.0",
"axios": "^1.4.0",
"cbor-js": "^0.1.0",
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"crypto-js": "^4.1.1",
"dom7": "^4.0.6",
"echarts": "^5.5.1",
"framework7": "^8.3.3",
"echarts": "^5.4.3",
"framework7": "^8.3.0",
"framework7-icons": "^5.0.5",
"framework7-vue": "^8.3.3",
"framework7-vue": "^8.3.0",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",
"line-awesome": "^1.3.0",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"pinia": "^2.1.7",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"pinia": "^2.1.6",
"register-service-worker": "^1.7.2",
"skeleton-elements": "^4.0.1",
"swiper": "^10.2.0",
"ua-parser-js": "^1.0.38",
"vue": "^3.4.31",
"vue-echarts": "^6.7.3",
"vue-i18n": "^9.13.1",
"vue-router": "^4.4.0",
"vue3-perfect-scrollbar": "^2.0.0",
"ua-parser-js": "^1.0.35",
"vue": "^3.3.4",
"vue-echarts": "^6.6.1",
"vue-i18n": "^9.2.2",
"vue-router": "^4.2.4",
"vue3-perfect-scrollbar": "^1.6.1",
"vuedraggable": "^4.1.0",
"vuetify": "^3.6.11"
"vuetify": "^3.3.16"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"@vitejs/plugin-vue": "^4.3.1",
"@vue/compiler-sfc": "^3.3.4",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.27.0",
"eslint": "^8.47.0",
"eslint-plugin-vue": "^9.17.0",
"git-rev-sync": "^3.0.2",
"postcss-preset-env": "^9.5.16",
"sass": "^1.77.6",
"vite": "^5.3.3",
"vite-plugin-pwa": "^0.20.0",
"vite-plugin-vuetify": "^2.0.3"
"postcss-preset-env": "^9.1.1",
"sass": "^1.66.1",
"vite": "^4.4.9",
"vite-plugin-pwa": "^0.16.4",
"vite-plugin-vuetify": "^1.0.2"
},
"browserslist": [
"> 1%",
+5 -55
View File
@@ -4,13 +4,10 @@ import (
"sort"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
@@ -146,14 +143,9 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
return nil, errs.ErrClientTimezoneOffsetInvalid
}
if accountCreateReq.Category < models.ACCOUNT_CATEGORY_CASH || accountCreateReq.Category > models.ACCOUNT_CATEGORY_INVESTMENT {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account category invalid, category is %d", accountCreateReq.Category)
return nil, errs.ErrAccountCategoryInvalid
}
if accountCreateReq.Type == models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
if len(accountCreateReq.SubAccounts) > 0 {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account cannot have any sub-accounts")
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account cannot have any sub accounts")
return nil, errs.ErrAccountCannotHaveSubAccounts
}
@@ -163,7 +155,7 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
}
} else if accountCreateReq.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS {
if len(accountCreateReq.SubAccounts) < 1 {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account does not have any sub-accounts")
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] account does not have any sub accounts")
return nil, errs.ErrAccountHaveNoSubAccount
}
@@ -181,17 +173,17 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
subAccount := accountCreateReq.SubAccounts[i]
if subAccount.Category != accountCreateReq.Category {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] category of sub-account not equals to parent")
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] category of sub account not equals to parent")
return nil, errs.ErrSubAccountCategoryNotEqualsToParent
}
if subAccount.Type != models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] sub-account type invalid")
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] sub account type invalid")
return nil, errs.ErrSubAccountTypeInvalid
}
if subAccount.Currency == validators.ParentAccountCurrencyPlaceholder {
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] sub-account cannot set currency placeholder")
log.WarnfWithRequestId(c, "[accounts.AccountCreateHandler] sub account cannot set currency placeholder")
return nil, errs.ErrAccountCurrencyInvalid
}
}
@@ -211,42 +203,6 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
mainAccount := a.createNewAccountModel(uid, &accountCreateReq, maxOrderId+1)
childrenAccounts := a.createSubAccountModels(uid, &accountCreateReq)
if settings.Container.Current.EnableDuplicateSubmissionsCheck && accountCreateReq.ClientSessionId != "" {
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId)
if found {
log.InfofWithRequestId(c, "[accounts.AccountCreateHandler] another account \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
accountId, err := utils.StringToInt64(remark)
if err == nil {
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountId)
if err != nil {
log.ErrorfWithRequestId(c, "[accounts.AccountCreateHandler] failed to get existed account \"id:%d\" for user \"uid:%d\", because %s", accountId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
accountMap := a.accounts.GetAccountMapByList(accountAndSubAccounts)
mainAccount, exists := accountMap[accountId]
if !exists {
return nil, errs.ErrOperationFailed
}
accountInfoResp := mainAccount.ToAccountInfoResponse()
for i := 0; i < len(accountAndSubAccounts); i++ {
if accountAndSubAccounts[i].ParentAccountId == mainAccount.AccountId {
subAccountResp := accountAndSubAccounts[i].ToAccountInfoResponse()
accountInfoResp.SubAccounts = append(accountInfoResp.SubAccounts, subAccountResp)
}
}
return accountInfoResp, nil
}
}
}
err = a.accounts.CreateAccounts(c, mainAccount, childrenAccounts, utcOffset)
if err != nil {
@@ -256,7 +212,6 @@ func (a *AccountsApi) AccountCreateHandler(c *core.Context) (any, *errs.Error) {
log.InfofWithRequestId(c, "[accounts.AccountCreateHandler] user \"uid:%d\" has created a new account \"id:%d\" successfully", uid, mainAccount.AccountId)
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT, uid, accountCreateReq.ClientSessionId, utils.Int64ToString(mainAccount.AccountId))
accountInfoResp := mainAccount.ToAccountInfoResponse()
if len(childrenAccounts) > 0 {
@@ -280,11 +235,6 @@ func (a *AccountsApi) AccountModifyHandler(c *core.Context) (any, *errs.Error) {
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
if accountModifyReq.Category < models.ACCOUNT_CATEGORY_CASH || accountModifyReq.Category > models.ACCOUNT_CATEGORY_INVESTMENT {
log.WarnfWithRequestId(c, "[accounts.AccountModifyHandler] account category invalid, category is %d", accountModifyReq.Category)
return nil, errs.ErrAccountCategoryInvalid
}
uid := c.GetCurrentUid()
accountAndSubAccounts, err := a.accounts.GetAccountAndSubAccountsByAccountId(c, uid, accountModifyReq.Id)
+13 -14
View File
@@ -77,7 +77,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (any, *errs.Error)
twoFactorEnable, err = a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, user.Uid)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to check two-factor setting for user \"uid:%d\", because %s", user.Uid, err.Error())
log.ErrorfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to check two factor setting for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrSystemError)
}
}
@@ -104,7 +104,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (any, *errs.Error)
log.InfofWithRequestId(c, "[authorizations.AuthorizeHandler] user \"uid:%d\" has logined, token type is %d, token will be expired at %d", user.Uid, claims.Type, claims.ExpiresAt)
authResp := a.getAuthResponse(c, token, twoFactorEnable, user)
authResp := a.getAuthResponse(token, twoFactorEnable, user)
return authResp, nil
}
@@ -122,7 +122,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (any, *er
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get two-factor setting for user \"uid:%d\", because %s", uid, err.Error())
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get two factor setting for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrSystemError)
}
@@ -165,9 +165,9 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (any, *er
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has authorized two-factor via passcode, token will be expired at %d", user.Uid, claims.ExpiresAt)
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has authorized two factor via passcode, token will be expired at %d", user.Uid, claims.ExpiresAt)
authResp := a.getAuthResponse(c, token, false, user)
authResp := a.getAuthResponse(token, false, user)
return authResp, nil
}
@@ -185,7 +185,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two-factor setting for user \"uid:%d\", because %s", uid, err.Error())
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two factor setting for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrSystemError)
}
@@ -213,7 +213,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
err = a.twoFactorAuthorizations.GetAndUseUserTwoFactorRecoveryCode(c, uid, credential.RecoveryCode, user.Salt)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two-factor recovery code for user \"uid:%d\", because %s", uid, err.Error())
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two factor recovery code for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrTwoFactorRecoveryCodeNotExist)
}
@@ -234,17 +234,16 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Cont
c.SetTextualToken(token)
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has authorized two-factor via recovery code \"%s\", token will be expired at %d", user.Uid, credential.RecoveryCode, claims.ExpiresAt)
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has authorized two factor via recovery code \"%s\", token will be expired at %d", user.Uid, credential.RecoveryCode, claims.ExpiresAt)
authResp := a.getAuthResponse(c, token, false, user)
authResp := a.getAuthResponse(token, false, user)
return authResp, nil
}
func (a *AuthorizationsApi) getAuthResponse(c *core.Context, token string, need2FA bool, user *models.User) *models.AuthResponse {
func (a *AuthorizationsApi) getAuthResponse(token string, need2FA bool, user *models.User) *models.AuthResponse {
return &models.AuthResponse{
Token: token,
Need2FA: need2FA,
User: user.ToUserBasicInfo(),
NotificationContent: settings.Container.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale()),
Token: token,
Need2FA: need2FA,
User: user.ToUserBasicInfo(),
}
}
+90 -126
View File
@@ -19,40 +19,103 @@ const pageCountForDataExport = 1000
// DataManagementsApi represents data management api
type DataManagementsApi struct {
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter
tokens *services.TokenService
users *services.UserService
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
tags *services.TransactionTagService
templates *services.TransactionTemplateService
exporter *converters.EzBookKeepingCSVFileExporter
tokens *services.TokenService
users *services.UserService
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
tags *services.TransactionTagService
}
// Initialize a data management api singleton instance
var (
DataManagements = &DataManagementsApi{
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{},
tokens: services.Tokens,
users: services.Users,
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
tags: services.TransactionTags,
templates: services.TransactionTemplates,
exporter: &converters.EzBookKeepingCSVFileExporter{},
tokens: services.Tokens,
users: services.Users,
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
tags: services.TransactionTags,
}
)
// ExportDataToEzbookkeepingCSVHandler returns exported data in csv format
func (a *DataManagementsApi) ExportDataToEzbookkeepingCSVHandler(c *core.Context) ([]byte, string, *errs.Error) {
return a.getExportedFileContent(c, "csv")
}
// ExportDataHandler returns exported data in csv format
func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string, *errs.Error) {
if !settings.Container.Current.EnableDataExport {
return nil, "", errs.ErrDataExportNotAllowed
}
// ExportDataToEzbookkeepingTSVHandler returns exported data in csv format
func (a *DataManagementsApi) ExportDataToEzbookkeepingTSVHandler(c *core.Context) ([]byte, string, *errs.Error) {
return a.getExportedFileContent(c, "tsv")
timezone := time.Local
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error())
} else {
timezone = time.FixedZone("Client Timezone", int(utcOffset)*60)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
}
return nil, "", errs.ErrUserNotFound
}
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
tags, err := a.tags.GetAllTagsByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
tagIndexs, err := a.tags.GetAllTagIdsOfAllTransactions(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
accountMap := a.accounts.GetAccountMapByList(accounts)
categoryMap := a.categories.GetCategoryMapByList(categories)
tagMap := a.tags.GetTagMapByList(tags)
allTransactions, err := a.transactions.GetAllTransactions(c, uid, pageCountForDataExport, true)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
result, err := a.exporter.ToExportedContent(uid, timezone, allTransactions, accountMap, categoryMap, tagMap, tagIndexs)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.Or(err, errs.ErrOperationFailed)
}
fileName := a.getFileName(user, timezone)
return result, fileName, nil
}
// DataStatisticsHandler returns user data statistics
@@ -86,19 +149,11 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.Context) (any, *errs.
return nil, errs.ErrOperationFailed
}
totalTransactionTemplateCount, err := a.templates.GetTotalNormalTemplateCountByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.DataStatisticsHandler] failed to get total transaction template count for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
dataStatisticsResp := &models.DataStatisticsResponse{
TotalAccountCount: totalAccountCount,
TotalTransactionCategoryCount: totalTransactionCategoryCount,
TotalTransactionTagCount: totalTransactionTagCount,
TotalTransactionCount: totalTransactionCount,
TotalTransactionTemplateCount: totalTransactionTemplateCount,
}
return dataStatisticsResp, nil
@@ -150,106 +205,15 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (any, *errs.Error
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.templates.DeleteAllTemplates(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ClearDataHandler] failed to delete all transaction templates, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[data_managements.ClearDataHandler] user \"uid:%d\" has cleared all data", uid)
return true, nil
}
func (a *DataManagementsApi) getExportedFileContent(c *core.Context, fileType string) ([]byte, string, *errs.Error) {
if !settings.Container.Current.EnableDataExport {
return nil, "", errs.ErrDataExportNotAllowed
}
timezone := time.Local
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error())
} else {
timezone = time.FixedZone("Client Timezone", int(utcOffset)*60)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
}
return nil, "", errs.ErrUserNotFound
}
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
tags, err := a.tags.GetAllTagsByUid(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
tagIndexes, err := a.tags.GetAllTagIdsMapOfAllTransactions(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
accountMap := a.accounts.GetAccountMapByList(accounts)
categoryMap := a.categories.GetCategoryMapByList(categories)
tagMap := a.tags.GetTagMapByList(tags)
allTransactions, err := a.transactions.GetAllTransactions(c, uid, pageCountForDataExport, true)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
var dataExporter converters.DataConverter
if fileType == "tsv" {
dataExporter = a.ezBookKeepingTsvExporter
} else {
dataExporter = a.ezBookKeepingCsvExporter
}
result, err := dataExporter.ToExportedContent(uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexes)
if err != nil {
log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error())
return nil, "", errs.Or(err, errs.ErrOperationFailed)
}
fileName := a.getFileName(user, timezone, fileType)
return result, fileName, nil
}
func (a *DataManagementsApi) getFileName(user *models.User, timezone *time.Location, fileExtension string) string {
func (a *DataManagementsApi) getFileName(user *models.User, timezone *time.Location) string {
currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), timezone)
currentTime = strings.Replace(currentTime, "-", "_", -1)
currentTime = strings.Replace(currentTime, " ", "_", -1)
currentTime = strings.Replace(currentTime, ":", "_", -1)
return fmt.Sprintf("%s_%s.%s", user.Username, currentTime, fileExtension)
return fmt.Sprintf("%s_%s.csv", user.Username, currentTime)
}
+1 -7
View File
@@ -2,7 +2,6 @@ package api
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"sort"
@@ -14,7 +13,6 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// ExchangeRatesApi represents exchange rate api
@@ -36,7 +34,6 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (any, *err
uid := c.GetCurrentUid()
transport := http.DefaultTransport.(*http.Transport).Clone()
utils.SetProxyUrl(transport, settings.Container.Current.ExchangeRatesProxy)
if settings.Container.Current.ExchangeRatesSkipTLSVerify {
transport.TLSClientConfig = &tls.Config{
@@ -53,10 +50,7 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.Context) (any, *err
exchangeRateResps := make([]*models.LatestExchangeRateResponse, 0, len(urls))
for i := 0; i < len(urls); i++ {
req, _ := http.NewRequest("GET", urls[i], nil)
req.Header.Set("User-Agent", fmt.Sprintf("ezBookkeeping/%s ", settings.Version))
resp, err := client.Do(req)
resp, err := client.Get(urls[i])
if err != nil {
log.ErrorfWithRequestId(c, "[exchange_rates.LatestExchangeRateHandler] failed to request latest exchange rate data for user \"uid:%d\", because %s", uid, err.Error())
+32 -88
View File
@@ -1,6 +1,7 @@
package api
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
@@ -9,18 +10,14 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const openStreetMapTileImageUrlFormat = "https://tile.openstreetmap.org/{z}/{x}/{y}.png" // https://tile.openstreetmap.org/{z}/{x}/{y}.png
const openStreetMapHumanitarianStyleTileImageUrlFormat = "https://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png" // https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png
const openTopoMapTileImageUrlFormat = "https://tile.opentopomap.org/{z}/{x}/{y}.png" // https://tile.opentopomap.org/{z}/{x}/{y}.png
const opnvKarteMapTileImageUrlFormat = "https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png" // https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png
const cyclOSMMapTileImageUrlFormat = "https://a.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png" // https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png
const cartoDBMapTileImageUrlFormat = "https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{scale}.png" // https://{s}.basemaps.cartocdn.com/{style}/{z}/{x}/{y}{scale}.png
const tomtomMapTileImageUrlFormat = "https://api.tomtom.com/map/1/tile/basic/main/{z}/{x}/{y}.png" // https://api.tomtom.com/map/{versionNumber}/tile/{layer}/{style}/{z}/{x}/{y}.png?key={key}&language={language}
const tianDiTuMapTileImageUrlFormat = "https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}" // https://t{s}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk={key}
const tianDiTuMapAnnotationUrlFormat = "https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}" // https://t{s}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk={key}
const openStreetMapTileImageUrlFormat = "https://tile.openstreetmap.org/%s/%s/%s" // https://tile.openstreetmap.org/{z}/{x}/{y}.png
const openStreetMapHumanitarianStyleTileImageUrlFormat = "https://a.tile.openstreetmap.fr/hot/%s/%s/%s" // https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png
const openTopoMapTileImageUrlFormat = "https://tile.opentopomap.org/%s/%s/%s" // https://tile.opentopomap.org/{z}/{x}/{y}.png
const opnvKarteMapTileImageUrlFormat = "https://tileserver.memomaps.de/tilegen/%s/%s/%s" // https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png
const cyclOSMMapTileImageUrlFormat = "https://a.tile-cyclosm.openstreetmap.fr/cyclosm/%s/%s/%s" // https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png
const tomtomMapTileImageUrlFormat = "https://api.tomtom.com/map/1/tile/basic/main/%s/%s/%s" // https://api.tomtom.com/map/{versionNumber}/tile/{layer}/{style}/{z}/{x}/{y}.png?key={key}&language={language}
// MapImageProxy represents map image proxy
type MapImageProxy struct {
@@ -33,86 +30,36 @@ var (
// MapTileImageProxyHandler returns map tile image
func (p *MapImageProxy) MapTileImageProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
return p.mapImageProxyHandler(c, func(c *core.Context, mapProvider string) (string, *errs.Error) {
if mapProvider == settings.OpenStreetMapProvider {
return openStreetMapTileImageUrlFormat, nil
} else if mapProvider == settings.OpenStreetMapHumanitarianStyleProvider {
return openStreetMapHumanitarianStyleTileImageUrlFormat, nil
} else if mapProvider == settings.OpenTopoMapProvider {
return openTopoMapTileImageUrlFormat, nil
} else if mapProvider == settings.OPNVKarteMapProvider {
return opnvKarteMapTileImageUrlFormat, nil
} else if mapProvider == settings.CyclOSMMapProvider {
return cyclOSMMapTileImageUrlFormat, nil
} else if mapProvider == settings.CartoDBMapProvider {
return cartoDBMapTileImageUrlFormat, nil
} else if mapProvider == settings.TomTomMapProvider {
targetUrl := tomtomMapTileImageUrlFormat + "?key=" + settings.Container.Current.TomTomMapAPIKey
language := c.Query("language")
if language != "" {
targetUrl = targetUrl + "&language=" + language
}
return targetUrl, nil
} else if mapProvider == settings.TianDiTuProvider {
return tianDiTuMapTileImageUrlFormat + "&tk=" + settings.Container.Current.TianDiTuAPIKey, nil
} else if mapProvider == settings.CustomProvider {
return settings.Container.Current.CustomMapTileServerTileLayerUrl, nil
}
return "", errs.ErrParameterInvalid
})
}
// MapAnnotationImageProxyHandler returns map annotation image
func (p *MapImageProxy) MapAnnotationImageProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
return p.mapImageProxyHandler(c, func(c *core.Context, mapProvider string) (string, *errs.Error) {
if mapProvider == settings.TianDiTuProvider {
return tianDiTuMapAnnotationUrlFormat + "&tk=" + settings.Container.Current.TianDiTuAPIKey, nil
} else if mapProvider == settings.CustomProvider {
return settings.Container.Current.CustomMapTileServerAnnotationLayerUrl, nil
}
return "", errs.ErrParameterInvalid
})
}
func (p *MapImageProxy) mapImageProxyHandler(c *core.Context, fn func(c *core.Context, mapProvider string) (string, *errs.Error)) (*httputil.ReverseProxy, *errs.Error) {
mapProvider := strings.Replace(c.Query("provider"), "-", "_", -1)
targetUrl := ""
if mapProvider != settings.Container.Current.MapProvider {
return nil, errs.ErrMapProviderNotCurrent
if mapProvider == settings.OpenStreetMapProvider {
targetUrl = openStreetMapTileImageUrlFormat
} else if mapProvider == settings.OpenStreetMapHumanitarianStyleProvider {
targetUrl = openStreetMapHumanitarianStyleTileImageUrlFormat
} else if mapProvider == settings.OpenTopoMapProvider {
targetUrl = openTopoMapTileImageUrlFormat
} else if mapProvider == settings.OPNVKarteMapProvider {
targetUrl = opnvKarteMapTileImageUrlFormat
} else if mapProvider == settings.CyclOSMMapProvider {
targetUrl = cyclOSMMapTileImageUrlFormat
} else if mapProvider == settings.TomTomMapProvider {
targetUrl = tomtomMapTileImageUrlFormat + "?key=" + settings.Container.Current.TomTomMapAPIKey
language := c.Query("language")
if language != "" {
targetUrl = targetUrl + "&language=" + language
}
} else {
return nil, errs.ErrParameterInvalid
}
zoomLevel := c.Param("zoomLevel")
coordinateX := c.Param("coordinateX")
fileName := c.Param("fileName")
fileNameParts := strings.Split(fileName, ".")
coordinateY := fileNameParts[0]
scale := c.Query("scale")
if len(fileNameParts) != 2 || fileNameParts[len(fileNameParts)-1] != "png" {
return nil, errs.ErrImageExtensionNotSupported
}
var err *errs.Error
targetUrl, err = fn(c, mapProvider)
if err != nil {
return nil, err
}
transport := http.DefaultTransport.(*http.Transport).Clone()
utils.SetProxyUrl(transport, settings.Container.Current.MapProxy)
director := func(req *http.Request) {
imageRawUrl := targetUrl
imageRawUrl = strings.Replace(imageRawUrl, "{z}", zoomLevel, -1)
imageRawUrl = strings.Replace(imageRawUrl, "{x}", coordinateX, -1)
imageRawUrl = strings.Replace(imageRawUrl, "{y}", coordinateY, -1)
imageRawUrl = strings.Replace(imageRawUrl, "{scale}", scale, -1)
zoomLevel := c.Param("zoomLevel")
coordinateX := c.Param("coordinateX")
fileName := c.Param("fileName")
imageRawUrl := fmt.Sprintf(targetUrl, zoomLevel, coordinateX, fileName)
imageUrl, _ := url.Parse(imageRawUrl)
req.URL = imageUrl
@@ -120,8 +67,5 @@ func (p *MapImageProxy) mapImageProxyHandler(c *core.Context, fn func(c *core.Co
req.Host = imageUrl.Host
}
return &httputil.ReverseProxy{
Transport: transport,
Director: director,
}, nil
return &httputil.ReverseProxy{Director: director}, nil
}
+1 -1
View File
@@ -35,7 +35,7 @@ func (a *QrCodesApi) MobileUrlQrCodeHandler(c *core.Context) ([]byte, string, *e
return nil, "", errs.ErrOperationFailed
}
return data, "image/png", nil
return data, "", nil
}
func (a *QrCodesApi) generateUrlQrCode(c *core.Context, url string) ([]byte, *errs.Error) {
+6 -41
View File
@@ -2,14 +2,12 @@ package api
import (
"sort"
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
@@ -46,7 +44,8 @@ func (a *TokensApi) TokenListHandler(c *core.Context) (any, *errs.Error) {
TokenId: a.tokens.GenerateTokenId(token),
TokenType: token.TokenType,
UserAgent: token.UserAgent,
LastSeen: token.LastSeenUnixTime,
CreatedAt: token.CreatedUnixTime,
ExpiredAt: token.ExpiredUnixTime,
}
if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
@@ -177,40 +176,6 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
return nil, errs.ErrUserNotFound
}
now := time.Now().Unix()
oldTokenClaims := c.GetTokenClaims()
if now-oldTokenClaims.IssuedAt < int64(settings.Container.Current.TokenMinRefreshInterval) {
log.InfofWithRequestId(c, "[token.TokenRefreshHandler] token of user \"uid:%d\" does not need to be refreshed", uid)
userTokenId, err := utils.StringToInt64(oldTokenClaims.UserTokenId)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.TokenRefreshHandler] parse user token id failed, because %s", err.Error())
} else {
tokenRecord := &models.TokenRecord{
Uid: oldTokenClaims.Uid,
UserTokenId: userTokenId,
CreatedUnixTime: oldTokenClaims.IssuedAt,
}
tokenId := a.tokens.GenerateTokenId(tokenRecord)
err = a.tokens.UpdateTokenLastSeen(c, tokenRecord)
if err != nil {
log.WarnfWithRequestId(c, "[token.TokenRefreshHandler] failed to update last seen of token \"id:%s\" for user \"uid:%d\", because %s", tokenId, uid, err.Error())
}
}
refreshResp := &models.TokenRefreshResponse{
User: user.ToUserBasicInfo(),
NotificationContent: settings.Container.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
}
return refreshResp, nil
}
token, claims, err := a.tokens.CreateToken(c, user)
if err != nil {
@@ -218,6 +183,7 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
return nil, errs.Or(err, errs.ErrTokenGenerating)
}
oldTokenClaims := c.GetTokenClaims()
oldUserTokenId, _ := utils.StringToInt64(oldTokenClaims.UserTokenId)
oldTokenRecord := &models.TokenRecord{
Uid: uid,
@@ -231,10 +197,9 @@ func (a *TokensApi) TokenRefreshHandler(c *core.Context) (any, *errs.Error) {
log.InfofWithRequestId(c, "[token.TokenRefreshHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
refreshResp := &models.TokenRefreshResponse{
NewToken: token,
OldTokenId: a.tokens.GenerateTokenId(oldTokenRecord),
User: user.ToUserBasicInfo(),
NotificationContent: settings.Container.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
NewToken: token,
OldTokenId: a.tokens.GenerateTokenId(oldTokenRecord),
User: user.ToUserBasicInfo(),
}
return refreshResp, nil
+9 -68
View File
@@ -6,13 +6,10 @@ import (
"github.com/gin-gonic/gin/binding"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// TransactionCategoriesApi represents transaction category api
@@ -122,28 +119,6 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any,
category := a.createNewCategoryModel(uid, &categoryCreateReq, maxOrderId+1)
if settings.Container.Current.EnableDuplicateSubmissionsCheck && categoryCreateReq.ClientSessionId != "" {
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId)
if found {
log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateHandler] another category \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
categoryId, err := utils.StringToInt64(remark)
if err == nil {
category, err = a.categories.GetCategoryByCategoryId(c, uid, categoryId)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryCreateHandler] failed to get existed category \"id:%d\" for user \"uid:%d\", because %s", categoryId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
categoryResp := category.ToTransactionCategoryInfoResponse()
return categoryResp, nil
}
}
}
err = a.categories.CreateCategory(c, category)
if err != nil {
@@ -153,7 +128,6 @@ func (a *TransactionCategoriesApi) CategoryCreateHandler(c *core.Context) (any,
log.InfofWithRequestId(c, "[transaction_categories.CategoryCreateHandler] user \"uid:%d\" has created a new category \"id:%d\" successfully", uid, category.CategoryId)
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_CATEGORY, uid, categoryCreateReq.ClientSessionId, utils.Int64ToString(category.CategoryId))
categoryResp := category.ToTransactionCategoryInfoResponse()
return categoryResp, nil
@@ -199,18 +173,16 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any,
}
newCategory := &models.TransactionCategory{
CategoryId: category.CategoryId,
Uid: uid,
ParentCategoryId: categoryModifyReq.ParentId,
Name: categoryModifyReq.Name,
Icon: categoryModifyReq.Icon,
Color: categoryModifyReq.Color,
Comment: categoryModifyReq.Comment,
Hidden: categoryModifyReq.Hidden,
CategoryId: category.CategoryId,
Uid: uid,
Name: categoryModifyReq.Name,
Icon: categoryModifyReq.Icon,
Color: categoryModifyReq.Color,
Comment: categoryModifyReq.Comment,
Hidden: categoryModifyReq.Hidden,
}
if newCategory.ParentCategoryId == category.ParentCategoryId &&
newCategory.Name == category.Name &&
if newCategory.Name == category.Name &&
newCategory.Icon == category.Icon &&
newCategory.Color == category.Color &&
newCategory.Comment == category.Comment &&
@@ -218,38 +190,6 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any,
return nil, errs.ErrNothingWillBeUpdated
}
if category.ParentCategoryId == 0 && newCategory.ParentCategoryId != 0 {
return nil, errs.Or(err, errs.ErrNotAllowChangePrimaryTransactionCategoryToSecondary)
}
if category.ParentCategoryId != 0 && newCategory.ParentCategoryId == 0 {
return nil, errs.Or(err, errs.ErrNotAllowChangeSecondaryTransactionCategoryToPrimary)
}
if newCategory.ParentCategoryId != category.ParentCategoryId {
fromPrimaryCategory, err := a.categories.GetCategoryByCategoryId(c, uid, category.ParentCategoryId)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] failed to get old primary category \"id:%d\" of category \"id:%d\" for user \"uid:%d\", because %s", category.ParentCategoryId, categoryModifyReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
toPrimaryCategory, err := a.categories.GetCategoryByCategoryId(c, uid, newCategory.ParentCategoryId)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_categories.CategoryModifyHandler] failed to get new primary category \"id:%d\" of category \"id:%d\" for user \"uid:%d\", because %s", newCategory.ParentCategoryId, categoryModifyReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if fromPrimaryCategory.Type != toPrimaryCategory.Type {
return nil, errs.Or(err, errs.ErrNotAllowChangePrimaryTransactionType)
}
if toPrimaryCategory.ParentCategoryId != 0 {
return nil, errs.Or(err, errs.ErrNotAllowUseSecondaryTransactionAsPrimaryCategory)
}
}
err = a.categories.ModifyCategory(c, newCategory)
if err != nil {
@@ -260,6 +200,7 @@ func (a *TransactionCategoriesApi) CategoryModifyHandler(c *core.Context) (any,
log.InfofWithRequestId(c, "[transaction_categories.CategoryModifyHandler] user \"uid:%d\" has updated category \"id:%d\" successfully", uid, categoryModifyReq.Id)
newCategory.Type = category.Type
newCategory.ParentCategoryId = category.ParentCategoryId
newCategory.DisplayOrder = category.DisplayOrder
categoryResp := newCategory.ToTransactionCategoryInfoResponse()
+6 -6
View File
@@ -150,7 +150,7 @@ func (a *TransactionTagsApi) TagHideHandler(c *core.Context) (any, *errs.Error)
err := c.ShouldBindJSON(&tagHideReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_tags.TagHideHandler] parse request failed, because %s", err.Error())
log.WarnfWithRequestId(c, "[transaction_tags.CategoryHideHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
@@ -158,11 +158,11 @@ func (a *TransactionTagsApi) TagHideHandler(c *core.Context) (any, *errs.Error)
err = a.tags.HideTag(c, uid, []int64{tagHideReq.Id}, tagHideReq.Hidden)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagHideHandler] failed to hide tag \"id:%d\" for user \"uid:%d\", because %s", tagHideReq.Id, uid, err.Error())
log.ErrorfWithRequestId(c, "[transaction_tags.CategoryHideHandler] failed to hide tag \"id:%d\" for user \"uid:%d\", because %s", tagHideReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_tags.TagHideHandler] user \"uid:%d\" has hidden tag \"id:%d\"", uid, tagHideReq.Id)
log.InfofWithRequestId(c, "[transaction_tags.CategoryHideHandler] user \"uid:%d\" has hidden category \"id:%d\"", uid, tagHideReq.Id)
return true, nil
}
@@ -172,7 +172,7 @@ func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (any, *errs.Error)
err := c.ShouldBindJSON(&tagMoveReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_tags.TagMoveHandler] parse request failed, because %s", err.Error())
log.WarnfWithRequestId(c, "[transaction_tags.CategoryMoveHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
@@ -193,11 +193,11 @@ func (a *TransactionTagsApi) TagMoveHandler(c *core.Context) (any, *errs.Error)
err = a.tags.ModifyTagDisplayOrders(c, uid, tags)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_tags.TagMoveHandler] failed to move tags for user \"uid:%d\", because %s", uid, err.Error())
log.ErrorfWithRequestId(c, "[transaction_tags.CategoryMoveHandler] failed to move tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_tags.TagMoveHandler] user \"uid:%d\" has moved tags", uid)
log.InfofWithRequestId(c, "[transaction_tags.CategoryMoveHandler] user \"uid:%d\" has moved categories", uid)
return true, nil
}
-321
View File
@@ -1,321 +0,0 @@
package api
import (
"sort"
"strings"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// TransactionTemplatesApi represents transaction template api
type TransactionTemplatesApi struct {
templates *services.TransactionTemplateService
}
// Initialize a transaction template api singleton instance
var (
TransactionTemplates = &TransactionTemplatesApi{
templates: services.TransactionTemplates,
}
)
// TemplateListHandler returns transaction template list of current user
func (a *TransactionTemplatesApi) TemplateListHandler(c *core.Context) (any, *errs.Error) {
var templateListReq models.TransactionTemplateListRequest
err := c.ShouldBindQuery(&templateListReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateListHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
if templateListReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateListReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_NORMAL {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateListHandler] template type invalid, type is %d", templateListReq.TemplateType)
return nil, errs.ErrTransactionTemplateTypeInvalid
}
uid := c.GetCurrentUid()
templates, err := a.templates.GetAllTemplatesByUid(c, uid, templateListReq.TemplateType)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateListHandler] failed to get templates for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
templateResps := make(models.TransactionTemplateInfoResponseSlice, len(templates))
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
for i := 0; i < len(templates); i++ {
templateResps[i] = templates[i].ToTransactionTemplateInfoResponse(serverUtcOffset)
}
sort.Sort(templateResps)
return templateResps, nil
}
// TemplateGetHandler returns one specific transaction template of current user
func (a *TransactionTemplatesApi) TemplateGetHandler(c *core.Context) (any, *errs.Error) {
var templateGetReq models.TransactionTemplateGetRequest
err := c.ShouldBindQuery(&templateGetReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateGetHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateGetReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateGetHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateGetReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
return templateResp, nil
}
// TemplateCreateHandler saves a new transaction template by request parameters for current user
func (a *TransactionTemplatesApi) TemplateCreateHandler(c *core.Context) (any, *errs.Error) {
var templateCreateReq models.TransactionTemplateCreateRequest
err := c.ShouldBindJSON(&templateCreateReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
if templateCreateReq.TemplateType < models.TRANSACTION_TEMPLATE_TYPE_NORMAL || templateCreateReq.TemplateType > models.TRANSACTION_TEMPLATE_TYPE_NORMAL {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] template type invalid, type is %d", templateCreateReq.TemplateType)
return nil, errs.ErrTransactionTemplateTypeInvalid
}
if templateCreateReq.Type <= models.TRANSACTION_TYPE_MODIFY_BALANCE || templateCreateReq.Type > models.TRANSACTION_TYPE_TRANSFER {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] transaction type invalid, type is %d", templateCreateReq.Type)
return nil, errs.ErrTransactionTypeInvalid
}
uid := c.GetCurrentUid()
maxOrderId, err := a.templates.GetMaxDisplayOrder(c, uid, templateCreateReq.TemplateType)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
template := a.createNewTemplateModel(uid, &templateCreateReq, maxOrderId+1)
if settings.Container.Current.EnableDuplicateSubmissionsCheck && templateCreateReq.ClientSessionId != "" {
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId)
if found {
log.InfofWithRequestId(c, "[transaction_templates.TemplateCreateHandler] another template \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
templateId, err := utils.StringToInt64(remark)
if err == nil {
template, err = a.templates.GetTemplateByTemplateId(c, uid, templateId)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to get existed template \"id:%d\" for user \"uid:%d\", because %s", templateId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
return templateResp, nil
}
}
}
err = a.templates.CreateTemplate(c, template)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateCreateHandler] failed to create template \"id:%d\" for user \"uid:%d\", because %s", template.TemplateId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_templates.TemplateCreateHandler] user \"uid:%d\" has created a new template \"id:%d\" successfully", uid, template.TemplateId)
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId, utils.Int64ToString(template.TemplateId))
templateResp := template.ToTransactionTemplateInfoResponse(serverUtcOffset)
return templateResp, nil
}
// TemplateModifyHandler saves an existed transaction template by request parameters for current user
func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.Context) (any, *errs.Error) {
var templateModifyReq models.TransactionTemplateModifyRequest
err := c.ShouldBindJSON(&templateModifyReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
if templateModifyReq.Type <= models.TRANSACTION_TYPE_MODIFY_BALANCE || templateModifyReq.Type > models.TRANSACTION_TYPE_TRANSFER {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] transaction type invalid, type is %d", templateModifyReq.Type)
return nil, errs.ErrTransactionTypeInvalid
}
uid := c.GetCurrentUid()
template, err := a.templates.GetTemplateByTemplateId(c, uid, templateModifyReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] failed to get template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
newTemplate := &models.TransactionTemplate{
TemplateId: template.TemplateId,
Uid: uid,
Name: templateModifyReq.Name,
Type: templateModifyReq.Type,
CategoryId: templateModifyReq.CategoryId,
AccountId: templateModifyReq.SourceAccountId,
TagIds: strings.Join(templateModifyReq.TagIds, ","),
Amount: templateModifyReq.SourceAmount,
RelatedAccountId: templateModifyReq.DestinationAccountId,
RelatedAccountAmount: templateModifyReq.DestinationAmount,
HideAmount: templateModifyReq.HideAmount,
Comment: templateModifyReq.Comment,
}
if newTemplate.Name == template.Name &&
newTemplate.Type == template.Type &&
newTemplate.CategoryId == template.CategoryId &&
newTemplate.AccountId == template.AccountId &&
newTemplate.TagIds == template.TagIds &&
newTemplate.Amount == template.Amount &&
newTemplate.RelatedAccountId == template.RelatedAccountId &&
newTemplate.RelatedAccountAmount == template.RelatedAccountAmount &&
newTemplate.HideAmount == template.HideAmount &&
newTemplate.Comment == template.Comment {
return nil, errs.ErrNothingWillBeUpdated
}
err = a.templates.ModifyTemplate(c, newTemplate)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateModifyHandler] failed to update template \"id:%d\" for user \"uid:%d\", because %s", templateModifyReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_templates.TemplateModifyHandler] user \"uid:%d\" has updated template \"id:%d\" successfully", uid, templateModifyReq.Id)
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
newTemplate.TemplateType = template.TemplateType
newTemplate.DisplayOrder = template.DisplayOrder
newTemplate.Hidden = template.Hidden
templateResp := newTemplate.ToTransactionTemplateInfoResponse(serverUtcOffset)
return templateResp, nil
}
// TemplateHideHandler hides an transaction template by request parameters for current user
func (a *TransactionTemplatesApi) TemplateHideHandler(c *core.Context) (any, *errs.Error) {
var templateHideReq models.TransactionTemplateHideRequest
err := c.ShouldBindJSON(&templateHideReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateHideHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
err = a.templates.HideTemplate(c, uid, []int64{templateHideReq.Id}, templateHideReq.Hidden)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateHideHandler] failed to hide template \"id:%d\" for user \"uid:%d\", because %s", templateHideReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_templates.TemplateHideHandler] user \"uid:%d\" has hidden template \"id:%d\"", uid, templateHideReq.Id)
return true, nil
}
// TemplateMoveHandler moves display order of existed transaction templates by request parameters for current user
func (a *TransactionTemplatesApi) TemplateMoveHandler(c *core.Context) (any, *errs.Error) {
var templateMoveReq models.TransactionTemplateMoveRequest
err := c.ShouldBindJSON(&templateMoveReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_templates.CategoryMoveHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
templates := make([]*models.TransactionTemplate, len(templateMoveReq.NewDisplayOrders))
for i := 0; i < len(templateMoveReq.NewDisplayOrders); i++ {
newDisplayOrder := templateMoveReq.NewDisplayOrders[i]
template := &models.TransactionTemplate{
Uid: uid,
TemplateId: newDisplayOrder.Id,
DisplayOrder: newDisplayOrder.DisplayOrder,
}
templates[i] = template
}
err = a.templates.ModifyTemplateDisplayOrders(c, uid, templates)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateMoveHandler] failed to move templates for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_templates.TemplateMoveHandler] user \"uid:%d\" has moved templates", uid)
return true, nil
}
// TemplateDeleteHandler deletes an existed transaction template by request parameters for current user
func (a *TransactionTemplatesApi) TemplateDeleteHandler(c *core.Context) (any, *errs.Error) {
var templateDeleteReq models.TransactionTemplateDeleteRequest
err := c.ShouldBindJSON(&templateDeleteReq)
if err != nil {
log.WarnfWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
err = a.templates.DeleteTemplate(c, uid, templateDeleteReq.Id)
if err != nil {
log.ErrorfWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] failed to delete template \"id:%d\" for user \"uid:%d\", because %s", templateDeleteReq.Id, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[transaction_templates.TemplateDeleteHandler] user \"uid:%d\" has deleted template \"id:%d\"", uid, templateDeleteReq.Id)
return true, nil
}
func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCreateReq *models.TransactionTemplateCreateRequest, order int32) *models.TransactionTemplate {
return &models.TransactionTemplate{
Uid: uid,
TemplateType: templateCreateReq.TemplateType,
Name: templateCreateReq.Name,
Type: templateCreateReq.Type,
CategoryId: templateCreateReq.CategoryId,
AccountId: templateCreateReq.SourceAccountId,
TagIds: strings.Join(templateCreateReq.TagIds, ","),
Amount: templateCreateReq.SourceAmount,
RelatedAccountId: templateCreateReq.DestinationAccountId,
RelatedAccountAmount: templateCreateReq.DestinationAmount,
HideAmount: templateCreateReq.HideAmount,
Comment: templateCreateReq.Comment,
DisplayOrder: order,
}
}
+145 -236
View File
@@ -7,15 +7,15 @@ import (
orderedmap "github.com/wk8/go-ordered-map/v2"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const pageCountForLoadTransactionAmounts = 1000
// TransactionsApi represents transaction api
type TransactionsApi struct {
transactions *services.TransactionService
@@ -48,33 +48,21 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.Context) (any, *errs.E
uid := c.GetCurrentUid()
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionCountReq.AccountIds, uid)
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionCountReq.AccountId, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionCountHandler] get account error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionCountReq.CategoryIds, uid)
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionCountReq.CategoryId, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionCountHandler] get transaction category error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
var allTagIds []int64
noTags := transactionCountReq.TagIds == "none"
if !noTags {
allTagIds, err = a.getTagIds(transactionCountReq.TagIds)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionCountHandler] get transaction tag ids error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionCountReq.AmountFilter, transactionCountReq.Keyword)
totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, transactionCountReq.Keyword)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionCountHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
@@ -116,36 +104,24 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (any, *errs.Er
return nil, errs.ErrUserNotFound
}
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionListReq.AccountIds, uid)
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionListReq.AccountId, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionListHandler] get account error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionListReq.CategoryIds, uid)
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionListReq.CategoryId, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionListHandler] get transaction category error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
var allTagIds []int64
noTags := transactionListReq.TagIds == "none"
if !noTags {
allTagIds, err = a.getTagIds(transactionListReq.TagIds)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionListHandler] get transaction tag ids error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
var totalCount int64
if transactionListReq.WithCount {
totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword)
totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
@@ -153,7 +129,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.Context) (any, *errs.Er
}
}
transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true)
transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionListHandler] failed to get transactions earlier than \"%d\" for user \"uid:%d\", because %s", transactionListReq.MaxTime, uid, err.Error())
@@ -219,33 +195,21 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.Context) (any, *er
return nil, errs.ErrUserNotFound
}
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionListReq.AccountIds, uid)
allAccountIds, err := a.getAccountOrSubAccountIds(c, transactionListReq.AccountId, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthListHandler] get account error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionListReq.CategoryIds, uid)
allCategoryIds, err := a.getCategoryOrSubCategoryIds(c, transactionListReq.CategoryId, uid)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthListHandler] get transaction category error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
var allTagIds []int64
noTags := transactionListReq.TagIds == "none"
if !noTags {
allTagIds, err = a.getTagIds(transactionListReq.TagIds)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthListHandler] get transaction tag ids error, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
}
transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, allTagIds, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword)
transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, transactionListReq.Keyword)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionMonthListHandler] failed to get transactions in month \"%d-%d\" for user \"uid:%d\", because %s", transactionListReq.Year, transactionListReq.Month, uid, err.Error())
@@ -277,15 +241,8 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (any, *e
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionStatisticsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(c, uid, statisticReq.StartTime, statisticReq.EndTime, utcOffset, statisticReq.UseTransactionTimezone)
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(c, uid, statisticReq.StartTime, statisticReq.EndTime)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionStatisticsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
@@ -311,64 +268,6 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.Context) (any, *e
return statisticResp, nil
}
// TransactionStatisticsTrendsHandler returns transaction statistics trends of current user
func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.Context) (any, *errs.Error) {
var statisticTrendsReq models.TransactionStatisticTrendsRequest
err := c.ShouldBindQuery(&statisticTrendsReq)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionStatisticsTrendsHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionStatisticsTrendsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
startYear, startMonth, endYear, endMonth, err := statisticTrendsReq.GetNumericYearMonthRange()
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionStatisticsTrendsHandler] cannot parse year month, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
uid := c.GetCurrentUid()
allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyIncomeAndExpense(c, uid, startYear, startMonth, endYear, endMonth, utcOffset, statisticTrendsReq.UseTransactionTimezone)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionStatisticsTrendsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
statisticTrendsResp := make(models.TransactionStatisticTrendsItemSlice, 0, len(allMonthlyTotalAmounts))
for yearMonth, monthlyTotalAmounts := range allMonthlyTotalAmounts {
monthlyStatisticResp := &models.TransactionStatisticTrendsItem{
Year: yearMonth / 100,
Month: yearMonth % 100,
Items: make([]*models.TransactionStatisticResponseItem, len(monthlyTotalAmounts)),
}
for i := 0; i < len(monthlyTotalAmounts); i++ {
totalAmountItem := monthlyTotalAmounts[i]
monthlyStatisticResp.Items[i] = &models.TransactionStatisticResponseItem{
CategoryId: totalAmountItem.CategoryId,
AccountId: totalAmountItem.AccountId,
TotalAmount: totalAmountItem.Amount,
}
}
statisticTrendsResp = append(statisticTrendsResp, monthlyStatisticResp)
}
sort.Sort(statisticTrendsResp)
return statisticTrendsResp, nil
}
// TransactionAmountsHandler returns transaction amounts of current user
func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (any, *errs.Error) {
var transactionAmountsReq models.TransactionAmountsRequest
@@ -396,13 +295,6 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (any, *errs
return nil, errs.ErrQueryItemsTooMuch
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
@@ -418,7 +310,7 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (any, *errs
for i := 0; i < len(requestItems); i++ {
requestItem := requestItems[i]
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime, utcOffset, transactionAmountsReq.UseTransactionTimezone)
incomeAmounts, expenseAmounts, err := a.transactions.GetAccountsTotalIncomeAndExpense(c, uid, requestItem.StartTime, requestItem.EndTime)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionAmountsHandler] failed to get transaction amounts item for user \"uid:%d\", because %s", uid, err.Error())
@@ -489,6 +381,120 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (any, *errs
return amountsResp, nil
}
// TransactionMonthAmountsHandler returns every month transaction amounts of current user
func (a *TransactionsApi) TransactionMonthAmountsHandler(c *core.Context) (any, *errs.Error) {
var transactionAmountsReq models.TransactionMonthAmountsRequest
err := c.ShouldBindQuery(&transactionAmountsReq)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
startTime, endTime, err := transactionAmountsReq.GetStartTimeAndEndTime(utcOffset)
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] parse request start or end date failed, because %s", err.Error())
return nil, errs.ErrParameterInvalid
}
uid := c.GetCurrentUid()
accounts, err := a.accounts.GetAllAccountsByUid(c, uid)
accountMap := a.accounts.GetAccountMapByList(accounts)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
totalAmounts, err := a.transactions.GetAccountsMonthTotalIncomeAndExpense(c, uid, startTime, endTime, pageCountForLoadTransactionAmounts)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] failed to get accounts month total income and expense for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
amountsMap := make(map[string]map[string]*models.TransactionAmountsResponseItemAmountInfo)
for yearMonth, monthAccountsAmounts := range totalAmounts {
for accountId, monthAccountAmounts := range monthAccountsAmounts {
account, exists := accountMap[accountId]
if !exists {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] cannot find account for account \"id:%d\" of user \"uid:%d\"", accountId, uid)
continue
}
monthTotalAmounts, exists := amountsMap[yearMonth]
if !exists {
monthTotalAmounts = make(map[string]*models.TransactionAmountsResponseItemAmountInfo)
amountsMap[yearMonth] = monthTotalAmounts
}
monthTotalAmount, exists := monthTotalAmounts[account.Currency]
if !exists {
monthTotalAmount = &models.TransactionAmountsResponseItemAmountInfo{
Currency: account.Currency,
IncomeAmount: 0,
ExpenseAmount: 0,
}
}
monthTotalAmount.IncomeAmount += monthAccountAmounts.TotalIncomeAmount
monthTotalAmount.ExpenseAmount += monthAccountAmounts.TotalExpenseAmount
monthTotalAmounts[account.Currency] = monthTotalAmount
}
}
amountsResp := make(models.TransactionMonthAmountsResponseItemSlice, 0)
for yearMonth, monthTotalAmounts := range amountsMap {
yearMonthItems := strings.Split(yearMonth, "-")
year, err := utils.StringToInt32(yearMonthItems[0])
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] cannot get year from year-month item \"%s\" for user \"uid:%d\"", yearMonth, uid)
continue
}
month, err := utils.StringToInt32(yearMonthItems[1])
if err != nil {
log.WarnfWithRequestId(c, "[transactions.TransactionMonthAmountsHandler] cannot get month from year-month item \"%s\" for user \"uid:%d\"", yearMonth, uid)
continue
}
amounts := make(models.TransactionAmountsResponseItemAmountInfoSlice, 0, len(monthTotalAmounts))
for _, monthTotalAmount := range monthTotalAmounts {
amounts = append(amounts, monthTotalAmount)
}
sort.Sort(amounts)
amountsResp = append(amountsResp, &models.TransactionMonthAmountsResponseItem{
Year: year,
Month: month,
Amounts: amounts,
})
}
sort.Sort(amountsResp)
return amountsResp, nil
}
// TransactionGetHandler returns one specific transaction of current user
func (a *TransactionsApi) TransactionGetHandler(c *core.Context) (any, *errs.Error) {
var transactionGetReq models.TransactionGetRequest
@@ -663,28 +669,6 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (any, *errs.
return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
}
if settings.Container.Current.EnableDuplicateSubmissionsCheck && transactionCreateReq.ClientSessionId != "" {
found, remark := duplicatechecker.Container.Get(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION, uid, transactionCreateReq.ClientSessionId)
if found {
log.InfofWithRequestId(c, "[transactions.TransactionCreateHandler] another transaction \"id:%s\" has been created for user \"uid:%d\"", remark, uid)
transactionId, err := utils.StringToInt64(remark)
if err == nil {
transaction, err = a.transactions.GetTransactionByTransactionId(c, uid, transactionId)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionCreateHandler] failed to get existed transaction \"id:%d\" for user \"uid:%d\", because %s", transactionId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable)
return transactionResp, nil
}
}
}
err = a.transactions.CreateTransaction(c, transaction, tagIds)
if err != nil {
@@ -694,7 +678,6 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.Context) (any, *errs.
log.InfofWithRequestId(c, "[transactions.TransactionCreateHandler] user \"uid:%d\" has created a new transaction \"id:%d\" successfully", uid, transaction.TransactionId)
duplicatechecker.Container.Set(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION, uid, transactionCreateReq.ClientSessionId, utils.Int64ToString(transaction.TransactionId))
transactionResp := transaction.ToTransactionInfoResponse(tagIds, transactionEditable)
return transactionResp, nil
@@ -805,7 +788,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.Context) (any, *errs.
return nil, errs.ErrCannotModifyTransactionWithThisTransactionTime
}
err = a.transactions.ModifyTransaction(c, newTransaction, len(transactionTagIds), addTransactionTagIds, removeTransactionTagIds)
err = a.transactions.ModifyTransaction(c, newTransaction, addTransactionTagIds, removeTransactionTagIds)
if err != nil {
log.ErrorfWithRequestId(c, "[transactions.TransactionModifyHandler] failed to update transaction \"id:%d\" for user \"uid:%d\", because %s", transactionModifyReq.Id, uid, err.Error())
@@ -901,124 +884,50 @@ func (a *TransactionsApi) filterTransactions(c *core.Context, uid int64, transac
return finalTransactions
}
func (a *TransactionsApi) getAccountOrSubAccountIds(c *core.Context, accountIds string, uid int64) ([]int64, error) {
if accountIds == "" || accountIds == "0" {
return nil, nil
}
requestAccountIds, err := utils.StringArrayToInt64Array(strings.Split(accountIds, ","))
if err != nil {
return nil, errs.Or(err, errs.ErrAccountIdInvalid)
}
func (a *TransactionsApi) getAccountOrSubAccountIds(c *core.Context, accountId int64, uid int64) ([]int64, error) {
var allAccountIds []int64
if len(requestAccountIds) > 0 {
allSubAccounts, err := a.accounts.GetSubAccountsByAccountIds(c, uid, requestAccountIds)
if accountId > 0 {
allSubAccounts, err := a.accounts.GetSubAccountsByAccountId(c, uid, accountId)
if err != nil {
return nil, err
}
accountIdsMap := make(map[int64]int32, len(requestAccountIds))
for i := 0; i < len(requestAccountIds); i++ {
accountIdsMap[requestAccountIds[i]] = 0
}
for i := 0; i < len(allSubAccounts); i++ {
subAccount := allSubAccounts[i]
if refCount, exists := accountIdsMap[subAccount.ParentAccountId]; exists {
accountIdsMap[subAccount.ParentAccountId] = refCount + 1
} else {
accountIdsMap[subAccount.ParentAccountId] = 1
}
if _, exists := accountIdsMap[subAccount.AccountId]; exists {
delete(accountIdsMap, subAccount.AccountId)
}
allAccountIds = append(allAccountIds, subAccount.AccountId)
}
for accountId, refCount := range accountIdsMap {
if refCount < 1 {
allAccountIds = append(allAccountIds, accountId)
if len(allSubAccounts) > 0 {
for i := 0; i < len(allSubAccounts); i++ {
allAccountIds = append(allAccountIds, allSubAccounts[i].AccountId)
}
} else {
allAccountIds = append(allAccountIds, accountId)
}
}
return allAccountIds, nil
}
func (a *TransactionsApi) getCategoryOrSubCategoryIds(c *core.Context, categoryIds string, uid int64) ([]int64, error) {
if categoryIds == "" || categoryIds == "0" {
return nil, nil
}
requestCategoryIds, err := utils.StringArrayToInt64Array(strings.Split(categoryIds, ","))
if err != nil {
return nil, errs.Or(err, errs.ErrTransactionCategoryIdInvalid)
}
func (a *TransactionsApi) getCategoryOrSubCategoryIds(c *core.Context, categoryId int64, uid int64) ([]int64, error) {
var allCategoryIds []int64
if len(requestCategoryIds) > 0 {
allSubCategories, err := a.transactionCategories.GetSubCategoriesByCategoryIds(c, uid, requestCategoryIds)
if categoryId > 0 {
allSubCategories, err := a.transactionCategories.GetAllCategoriesByUid(c, uid, 0, categoryId)
if err != nil {
return nil, err
}
categoryIdsMap := make(map[int64]int32, len(requestCategoryIds))
for i := 0; i < len(requestCategoryIds); i++ {
categoryIdsMap[requestCategoryIds[i]] = 0
}
for i := 0; i < len(allSubCategories); i++ {
subCategory := allSubCategories[i]
if refCount, exists := categoryIdsMap[subCategory.ParentCategoryId]; exists {
categoryIdsMap[subCategory.ParentCategoryId] = refCount + 1
} else {
categoryIdsMap[subCategory.ParentCategoryId] = 1
}
if _, exists := categoryIdsMap[subCategory.CategoryId]; exists {
delete(categoryIdsMap, subCategory.CategoryId)
}
allCategoryIds = append(allCategoryIds, subCategory.CategoryId)
}
for accountId, refCount := range categoryIdsMap {
if refCount < 1 {
allCategoryIds = append(allCategoryIds, accountId)
if len(allSubCategories) > 0 {
for i := 0; i < len(allSubCategories); i++ {
allCategoryIds = append(allCategoryIds, allSubCategories[i].CategoryId)
}
} else {
allCategoryIds = append(allCategoryIds, categoryId)
}
}
return allCategoryIds, nil
}
func (a *TransactionsApi) getTagIds(tagIds string) ([]int64, error) {
if tagIds == "" || tagIds == "0" {
return nil, nil
}
requestTagIds, err := utils.StringArrayToInt64Array(strings.Split(tagIds, ","))
if err != nil {
return nil, errs.Or(err, errs.ErrTransactionTagIdInvalid)
}
return requestTagIds, nil
}
func (a *TransactionsApi) getTransactionTagIds(allTransactionTagIds map[int64][]int64) []int64 {
allTagIds := make([]int64, 0, len(allTransactionTagIds))
+17 -17
View File
@@ -45,7 +45,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (an
}
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorStatusHandler] failed to get two-factor setting, because %s", err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorStatusHandler] failed to get two factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -63,7 +63,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
enabled, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to check two-factor setting, because %s", err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to check two factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -84,14 +84,14 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Conte
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(c, user)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two-factor secret, because %s", err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two factor secret, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
img, err := key.Image(240, 240)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two-factor qrcode, because %s", err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two factor qrcode, because %s", err.Error())
return nil, errs.ErrOperationFailed
}
@@ -123,7 +123,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
exists, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to check two-factor setting, because %s", err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to check two factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -154,25 +154,25 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to generate two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to generate two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(c, uid, recoveryCodes, user.Salt)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.twoFactorAuthorizations.CreateTwoFactorSetting(c, twoFactorSetting)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two-factor setting for user \"uid:%d\", because %s", uid, err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two factor setting for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" has enabled two-factor authorization", uid)
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" has enabled two factor authorization", uid)
now := time.Now().Unix()
err = a.tokens.DeleteTokensBeforeTime(c, uid, now)
@@ -236,7 +236,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (a
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to check two-factor setting, because %s", err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to check two factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -247,18 +247,18 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (a
err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two-factor recovery codes for user \"uid:%d\"", uid)
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two factor recovery codes for user \"uid:%d\"", uid)
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.twoFactorAuthorizations.DeleteTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two-factor setting for user \"uid:%d\"", uid)
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two factor setting for user \"uid:%d\"", uid)
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] user \"uid:%d\" has disabled two-factor authorization", uid)
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] user \"uid:%d\" has disabled two factor authorization", uid)
return true, nil
}
@@ -291,7 +291,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to check two-factor setting, because %s", err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to check two factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -302,14 +302,14 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to generate two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to generate two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(c, uid, recoveryCodes, user.Salt)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to create two-factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to create two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
@@ -317,7 +317,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
RecoveryCodes: recoveryCodes,
}
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] user \"uid:%d\" has regenerated two-factor recovery codes", uid)
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] user \"uid:%d\" has regenerated two factor recovery codes", uid)
return recoveryCodesResp, nil
}
+4 -276
View File
@@ -1,8 +1,6 @@
package api
import (
"io"
"os"
"strings"
"time"
@@ -10,13 +8,10 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/locales"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/storage"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
@@ -91,9 +86,8 @@ func (a *UsersApi) UserRegisterHandler(c *core.Context) (any, *errs.Error) {
authResp := &models.RegisterResponse{
AuthResponse: models.AuthResponse{
Need2FA: false,
User: user.ToUserBasicInfo(),
NotificationContent: settings.Container.GetAfterRegisterNotificationContent(user.Language, c.GetClientLocale()),
Need2FA: false,
User: user.ToUserBasicInfo(),
},
NeedVerifyEmail: settings.Container.Current.EnableUserVerifyEmail && settings.Container.Current.EnableUserForceVerifyEmail,
PresetCategoriesSaved: presetCategoriesSaved,
@@ -188,8 +182,6 @@ func (a *UsersApi) UserEmailVerifyHandler(c *core.Context) (any, *errs.Error) {
resp.NewToken = token
resp.User = user.ToUserBasicInfo()
resp.NotificationContent = settings.Container.GetAfterLoginNotificationContent(user.Language, c.GetClientLocale())
c.SetTextualToken(token)
c.SetTokenClaims(claims)
@@ -270,22 +262,12 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
}
if userUpdateReq.DefaultAccountId > 0 && userUpdateReq.DefaultAccountId != user.DefaultAccountId {
accountMap, err := a.accounts.GetAccountsByAccountIds(c, uid, []int64{userUpdateReq.DefaultAccountId})
accounts, err := a.accounts.GetAccountsByAccountIds(c, uid, []int64{userUpdateReq.DefaultAccountId})
if err != nil || len(accountMap) < 1 {
if err != nil || len(accounts) < 1 {
return nil, errs.Or(err, errs.ErrUserDefaultAccountIsInvalid)
}
if _, exists := accountMap[userUpdateReq.DefaultAccountId]; !exists {
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] account \"id:%d\" does not exist for user \"uid:%d\"", userUpdateReq.DefaultAccountId, uid)
return nil, errs.ErrUserDefaultAccountIsInvalid
}
if accountMap[userUpdateReq.DefaultAccountId].Hidden {
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] account \"id:%d\" is hidden of user \"uid:%d\"", userUpdateReq.DefaultAccountId, uid)
return nil, errs.ErrUserDefaultAccountIsHidden
}
user.DefaultAccountId = userUpdateReq.DefaultAccountId
userNew.DefaultAccountId = userUpdateReq.DefaultAccountId
anythingUpdate = true
@@ -354,81 +336,6 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
userNew.ShortTimeFormat = models.SHORT_TIME_FORMAT_INVALID
}
if userUpdateReq.DecimalSeparator != nil && *userUpdateReq.DecimalSeparator != user.DecimalSeparator {
user.DecimalSeparator = *userUpdateReq.DecimalSeparator
userNew.DecimalSeparator = *userUpdateReq.DecimalSeparator
anythingUpdate = true
} else {
userNew.DecimalSeparator = core.DECIMAL_SEPARATOR_INVALID
}
if userUpdateReq.DigitGroupingSymbol != nil && *userUpdateReq.DigitGroupingSymbol != user.DigitGroupingSymbol {
user.DigitGroupingSymbol = *userUpdateReq.DigitGroupingSymbol
userNew.DigitGroupingSymbol = *userUpdateReq.DigitGroupingSymbol
anythingUpdate = true
} else {
userNew.DigitGroupingSymbol = core.DIGIT_GROUPING_SYMBOL_INVALID
}
if userUpdateReq.DigitGrouping != nil && *userUpdateReq.DigitGrouping != user.DigitGrouping {
user.DigitGrouping = *userUpdateReq.DigitGrouping
userNew.DigitGrouping = *userUpdateReq.DigitGrouping
anythingUpdate = true
} else {
userNew.DigitGrouping = core.DIGIT_GROUPING_TYPE_INVALID
}
if userUpdateReq.CurrencyDisplayType != nil && *userUpdateReq.CurrencyDisplayType != user.CurrencyDisplayType {
user.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType
userNew.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType
anythingUpdate = true
} else {
userNew.CurrencyDisplayType = models.CURRENCY_DISPLAY_TYPE_INVALID
}
if userUpdateReq.ExpenseAmountColor != nil && *userUpdateReq.ExpenseAmountColor != user.ExpenseAmountColor {
user.ExpenseAmountColor = *userUpdateReq.ExpenseAmountColor
userNew.ExpenseAmountColor = *userUpdateReq.ExpenseAmountColor
anythingUpdate = true
} else {
userNew.ExpenseAmountColor = models.AMOUNT_COLOR_TYPE_INVALID
}
if userUpdateReq.IncomeAmountColor != nil && *userUpdateReq.IncomeAmountColor != user.IncomeAmountColor {
user.IncomeAmountColor = *userUpdateReq.IncomeAmountColor
userNew.IncomeAmountColor = *userUpdateReq.IncomeAmountColor
anythingUpdate = true
} else {
userNew.IncomeAmountColor = models.AMOUNT_COLOR_TYPE_INVALID
}
if modifyUserLanguage || userNew.DecimalSeparator != core.DECIMAL_SEPARATOR_INVALID || userNew.DigitGroupingSymbol != core.DIGIT_GROUPING_SYMBOL_INVALID {
decimalSeparator := userNew.DecimalSeparator
digitGroupingSymbol := userNew.DigitGroupingSymbol
if userNew.DecimalSeparator == core.DECIMAL_SEPARATOR_INVALID {
decimalSeparator = user.DecimalSeparator
}
if userNew.DigitGroupingSymbol == core.DIGIT_GROUPING_SYMBOL_INVALID {
digitGroupingSymbol = user.DigitGroupingSymbol
}
locale := user.Language
if modifyUserLanguage {
locale = userNew.Language
}
if locale == "" {
locale = c.GetClientLocale()
}
if locales.IsDecimalSeparatorEqualsDigitGroupingSymbol(decimalSeparator, digitGroupingSymbol, locale) {
return nil, errs.ErrDecimalSeparatorAndDigitGroupingSymbolCannotBeEqual
}
}
if !anythingUpdate {
return nil, errs.ErrNothingWillBeUpdated
}
@@ -501,128 +408,6 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
return resp, nil
}
// UserUpdateAvatarHandler saves user avatar by request parameters for current user
func (a *UsersApi) UserUpdateAvatarHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
form, err := c.MultipartForm()
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get multi-part form data for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrParameterInvalid
}
avatars := form.File["avatar"]
if len(avatars) < 1 {
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] there is no user avatar in request for user \"uid:%d\"", user.Uid)
return nil, errs.ErrNoUserAvatar
}
if avatars[0].Size < 1 {
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] the size of user avatar in request is zero for user \"uid:%d\"", user.Uid)
return nil, errs.ErrUserAvatarIsEmpty
}
fileExtension := utils.GetFileNameExtension(avatars[0].Filename)
if utils.GetImageContentType(fileExtension) == "" {
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] the file extension \"%s\" of user avatar in request is not supported for user \"uid:%d\"", fileExtension, user.Uid)
return nil, errs.ErrImageTypeNotSupported
}
avatarFile, err := avatars[0].Open()
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get avatar file from request for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrOperationFailed
}
defer avatarFile.Close()
err = storage.Container.SaveAvatar(user.Uid, avatarFile, fileExtension)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to save avatar file for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrOperationFailed
}
err = a.users.UpdateUserAvatar(c, user.Uid, fileExtension)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if fileExtension != user.CustomAvatarType {
err = storage.Container.DeleteAvatar(user.Uid, user.CustomAvatarType)
if err != nil {
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to delete old avatar with extension \"%s\" for user \"uid:%d\", because %s", user.CustomAvatarType, user.Uid, err.Error())
}
}
user.CustomAvatarType = fileExtension
userResp := user.ToUserProfileResponse()
return userResp, nil
}
// UserRemoveAvatarHandler removes user avatar by request parameters for current user
func (a *UsersApi) UserRemoveAvatarHandler(c *core.Context) (any, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
if user.CustomAvatarType == "" {
return nil, errs.ErrNothingWillBeUpdated
}
err = storage.Container.DeleteAvatar(user.Uid, user.CustomAvatarType)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to delete avatar file for user \"uid:%d\", because %s", user.Uid, err.Error())
exists, err := storage.Container.ExistsAvatar(user.Uid, user.CustomAvatarType)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to check whether avatar file exist for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrOperationFailed
}
if exists {
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to delete whether avatar file exist for user \"uid:%d\", the avatar file still exist", user.Uid)
return nil, errs.ErrOperationFailed
}
}
err = a.users.UpdateUserAvatar(c, user.Uid, "")
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
user.CustomAvatarType = ""
userResp := user.ToUserProfileResponse()
return userResp, nil
}
// UserSendVerifyEmailByUnloginUserHandler sends unlogin user verify email
func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any, *errs.Error) {
if !settings.Container.Current.EnableUserVerifyEmail {
@@ -722,60 +507,3 @@ func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (any
return true, nil
}
// UserGetAvatarHandler returns user avatar data for current user
func (a *UsersApi) UserGetAvatarHandler(c *core.Context) ([]byte, string, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to get user, because %s", err.Error())
}
return nil, "", errs.ErrUserNotFound
}
if user.CustomAvatarType == "" {
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user does not have avatar for user \"uid:%d\"", user.Uid)
return nil, "", errs.ErrUserAvatarNoExists
}
fileName := c.Param("fileName")
fileBaseName := utils.GetFileNameWithoutExtension(fileName)
if utils.Int64ToString(user.Uid) != fileBaseName {
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] cannot get other user avatar \"uid:%s\" for user \"uid:%d\"", fileBaseName, user.Uid)
return nil, "", errs.ErrUserIdInvalid
}
fileExtension := utils.GetFileNameExtension(fileName)
if user.CustomAvatarType != fileExtension {
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user avatar extension is invalid \"%s\" for user \"uid:%d\"", fileExtension, user.Uid)
return nil, "", errs.ErrUserAvatarNoExists
}
avatarFile, err := storage.Container.ReadAvatar(user.Uid, fileExtension)
if os.IsNotExist(err) {
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user avatar file not exist for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, "", errs.ErrUserAvatarNoExists
}
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to get user avatar object for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
defer avatarFile.Close()
avatarData, err := io.ReadAll(avatarFile)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to read user avatar object data for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, "", errs.ErrOperationFailed
}
return avatarData, utils.GetImageContentType(fileExtension), nil
}
+26 -113
View File
@@ -20,7 +20,6 @@ const pageCountForDataExport = 1000
// UserDataCli represents user data cli
type UserDataCli struct {
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
@@ -35,7 +34,6 @@ type UserDataCli struct {
var (
UserData = &UserDataCli{
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{},
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{},
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
@@ -393,7 +391,7 @@ func (l *UserDataCli) DisableUserTwoFactorAuthorization(c *cli.Context, username
enableTwoFactor, err := l.twoFactorAuthorizations.ExistsTwoFactorSetting(nil, uid)
if err != nil {
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to check two-factor setting, because %s", err.Error())
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to check two factor setting, because %s", err.Error())
return err
}
@@ -404,14 +402,14 @@ func (l *UserDataCli) DisableUserTwoFactorAuthorization(c *cli.Context, username
err = l.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(nil, uid)
if err != nil {
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to delete two-factor recovery codes for user \"%s\"", username)
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to delete two factor recovery codes for user \"%s\"", username)
return err
}
err = l.twoFactorAuthorizations.DeleteTwoFactorSetting(nil, uid)
if err != nil {
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to delete two-factor setting for user \"%s\"", username)
log.BootErrorf("[user_data.DisableUserTwoFactorAuthorization] failed to delete two factor setting for user \"%s\"", username)
return err
}
@@ -432,7 +430,7 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
return false, err
}
accountMap, categoryMap, tagMap, tagIndexes, tagIndexesMap, err := l.getUserEssentialData(uid, username)
accountMap, categoryMap, tagMap, tagIndexs, err := l.getUserEssentialData(uid, username)
if err != nil {
log.BootErrorf("[user_data.CheckTransactionAndAccount] failed to get essential data for user \"%s\", because %s", username, err.Error())
@@ -472,7 +470,7 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
return false, err
}
err = l.checkTransactionTag(c, transaction.TransactionId, tagIndexesMap, tagMap)
err = l.checkTransactionTag(c, transaction.TransactionId, tagIndexs, tagMap)
if err != nil {
return false, err
@@ -501,7 +499,7 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
balance = balance + transaction.Amount
} else {
log.BootErrorf("[user_data.CheckTransactionAndAccount] transaction type of transaction \"id:%d\" is invalid", transaction.TransactionId)
log.BootErrorf("[user_data.CheckAccountBalance] transaction type of transaction \"id:%d\" is invalid", transaction.TransactionId)
return false, errs.ErrOperationFailed
}
@@ -516,12 +514,12 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
}
if !exists && account.Balance != 0 {
log.BootErrorf("[user_data.CheckTransactionAndAccount] account \"id:%d\" balance is not correct, expected balance is %d, but there is no transaction actually", account.AccountId, account.Balance)
log.BootErrorf("[user_data.CheckAccountBalance] account \"id:%d\" balance is not correct, expected balance is %d, but there is no transaction actually", account.AccountId, account.Balance)
return false, errs.ErrOperationFailed
}
if account.Balance != actualBalance {
log.BootErrorf("[user_data.CheckTransactionAndAccount] account \"id:%d\" balance is not correct, expected balance is %d, but actual balance is %d", account.AccountId, account.Balance, actualBalance)
log.BootErrorf("[user_data.CheckAccountBalance] account \"id:%d\" balance is not correct, expected balance is %d, but actual balance is %d", account.AccountId, account.Balance, actualBalance)
return false, errs.ErrOperationFailed
}
}
@@ -530,91 +528,16 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string
_, exists := accountMap[accountId]
if !exists {
log.BootErrorf("[user_data.CheckTransactionAndAccount] account \"id:%d\" does not exist, but there are some transactions of this account actually, and actual balance is %d", accountId, actualBalance)
log.BootErrorf("[user_data.CheckAccountBalance] account \"id:%d\" does not exist, but there are some transactions of this account actually, and actual balance is %d", accountId, actualBalance)
return false, errs.ErrOperationFailed
}
}
for i := 0; i < len(tagIndexes); i++ {
tagIndex := tagIndexes[i]
if tagIndex.TransactionTime < 1 {
log.BootErrorf("[user_data.CheckTransactionAndAccount] transaction tag index \"id:%d\" does not have transaction time", tagIndex.TagIndexId)
return false, errs.ErrOperationFailed
}
}
return true, nil
}
// FixTransactionTagIndexWithTransactionTime fixes user transaction tag index data with transaction time
func (l *UserDataCli) FixTransactionTagIndexWithTransactionTime(c *cli.Context, username string) (bool, error) {
if username == "" {
log.BootErrorf("[user_data.FixTransactionTagIndexWithTransactionTime] user name is empty")
return false, errs.ErrUsernameIsEmpty
}
uid, err := l.getUserIdByUsername(c, username)
if err != nil {
log.BootErrorf("[user_data.FixTransactionTagIndexWithTransactionTime] error occurs when getting user id by user name")
return false, err
}
tagIndexes, err := l.tags.GetAllTagIdsOfAllTransactions(nil, uid)
if err != nil {
log.BootErrorf("[user_data.FixTransactionTagIndexWithTransactionTime] failed to get tag index for user \"%s\", because %s", username, err.Error())
return false, err
}
invalidTagIndexes := make([]*models.TransactionTagIndex, 0, len(tagIndexes))
for i := 0; i < len(tagIndexes); i++ {
tagIndex := tagIndexes[i]
if tagIndex.TransactionTime < 1 {
invalidTagIndexes = append(invalidTagIndexes, tagIndex)
}
}
if len(invalidTagIndexes) < 1 {
log.BootErrorf("[user_data.FixTransactionTagIndexWithTransactionTime] all user transaction tag index data has been checked, there is no problem with user data")
return false, errs.ErrOperationFailed
}
allTransactions, err := l.transactions.GetAllTransactions(nil, uid, pageCountForGettingTransactions, false)
if err != nil {
log.BootErrorf("[user_data.FixTransactionTagIndexWithTransactionTime] failed to all transactions for user \"%s\", because %s", username, err.Error())
return false, err
}
transactionMap := l.transactions.GetTransactionMapByList(allTransactions)
for i := 0; i < len(invalidTagIndexes); i++ {
tagIndex := invalidTagIndexes[i]
transaction, exists := transactionMap[tagIndex.TransactionId]
if !exists {
continue
}
tagIndex.TransactionTime = transaction.TransactionTime
}
err = l.tags.ModifyTagIndexTransactionTime(nil, uid, invalidTagIndexes)
if err != nil {
log.BootErrorf("[user_data.FixTransactionTagIndexWithTransactionTime] failed to update transaction tag index for user \"%s\", because %s", username, err.Error())
return false, err
}
return true, nil
}
// ExportTransaction returns csv file content according user all transactions
func (l *UserDataCli) ExportTransaction(c *cli.Context, username string, fileType string) ([]byte, error) {
func (l *UserDataCli) ExportTransaction(c *cli.Context, username string) ([]byte, error) {
if username == "" {
log.BootErrorf("[user_data.ExportTransaction] user name is empty")
return nil, errs.ErrUsernameIsEmpty
@@ -627,7 +550,7 @@ func (l *UserDataCli) ExportTransaction(c *cli.Context, username string, fileTyp
return nil, err
}
accountMap, categoryMap, tagMap, _, tagIndexesMap, err := l.getUserEssentialData(uid, username)
accountMap, categoryMap, tagMap, tagIndexs, err := l.getUserEssentialData(uid, username)
if err != nil {
log.BootErrorf("[user_data.ExportTransaction] failed to get essential data for user \"%s\", because %s", username, err.Error())
@@ -641,15 +564,7 @@ func (l *UserDataCli) ExportTransaction(c *cli.Context, username string, fileTyp
return nil, err
}
var dataExporter converters.DataConverter
if fileType == "tsv" {
dataExporter = l.ezBookKeepingTsvExporter
} else {
dataExporter = l.ezBookKeepingCsvExporter
}
result, err := dataExporter.ToExportedContent(uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexesMap)
result, err := l.ezBookKeepingCsvExporter.ToExportedContent(uid, time.Local, allTransactions, accountMap, categoryMap, tagMap, tagIndexs)
if err != nil {
log.BootErrorf("[user_data.ExportTransaction] failed to get csv format exported data for \"%s\", because %s", username, err.Error())
@@ -670,17 +585,17 @@ func (l *UserDataCli) getUserIdByUsername(c *cli.Context, username string) (int6
return user.Uid, nil
}
func (l *UserDataCli) getUserEssentialData(uid int64, username string) (accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, tagIndexes []*models.TransactionTagIndex, tagIndexesMap map[int64][]int64, err error) {
func (l *UserDataCli) getUserEssentialData(uid int64, username string) (accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, tagIndexs map[int64][]int64, err error) {
if uid <= 0 {
log.BootErrorf("[user_data.getUserEssentialData] user uid \"%d\" is invalid", uid)
return nil, nil, nil, nil, nil, errs.ErrUserIdInvalid
return nil, nil, nil, nil, errs.ErrUserIdInvalid
}
accounts, err := l.accounts.GetAllAccountsByUid(nil, uid)
if err != nil {
log.BootErrorf("[user_data.getUserEssentialData] failed to get accounts for user \"%s\", because %s", username, err.Error())
return nil, nil, nil, nil, nil, err
return nil, nil, nil, nil, err
}
accountMap = l.accounts.GetAccountMapByList(accounts)
@@ -689,7 +604,7 @@ func (l *UserDataCli) getUserEssentialData(uid int64, username string) (accountM
if err != nil {
log.BootErrorf("[user_data.getUserEssentialData] failed to get categories for user \"%s\", because %s", username, err.Error())
return nil, nil, nil, nil, nil, err
return nil, nil, nil, nil, err
}
categoryMap = l.categories.GetCategoryMapByList(categories)
@@ -698,21 +613,19 @@ func (l *UserDataCli) getUserEssentialData(uid int64, username string) (accountM
if err != nil {
log.BootErrorf("[user_data.getUserEssentialData] failed to get tags for user \"%s\", because %s", username, err.Error())
return nil, nil, nil, nil, nil, err
return nil, nil, nil, nil, err
}
tagMap = l.tags.GetTagMapByList(tags)
tagIndexes, err = l.tags.GetAllTagIdsOfAllTransactions(nil, uid)
tagIndexs, err = l.tags.GetAllTagIdsOfAllTransactions(nil, uid)
if err != nil {
log.BootErrorf("[user_data.getUserEssentialData] failed to get tag index for user \"%s\", because %s", username, err.Error())
return nil, nil, nil, nil, nil, err
return nil, nil, nil, nil, err
}
tagIndexesMap = l.tags.GetGroupedTransactionTagIds(tagIndexes)
return accountMap, categoryMap, tagMap, tagIndexes, tagIndexesMap, nil
return accountMap, categoryMap, tagMap, tagIndexs, nil
}
func (l *UserDataCli) checkTransactionAccount(c *cli.Context, transaction *models.Transaction, accountMap map[int64]*models.Account, accountHasChild map[int64]bool) error {
@@ -724,7 +637,7 @@ func (l *UserDataCli) checkTransactionAccount(c *cli.Context, transaction *model
}
if account.ParentAccountId == models.LevelOneAccountParentId && accountHasChild[account.AccountId] {
log.BootErrorf("[user_data.checkTransactionAccount] the account \"id:%d\" of transaction \"id:%d\" is not a sub-account", transaction.AccountId, transaction.TransactionId)
log.BootErrorf("[user_data.checkTransactionAccount] the account \"id:%d\" of transaction \"id:%d\" is not a sub account", transaction.AccountId, transaction.TransactionId)
return errs.ErrOperationFailed
}
@@ -737,7 +650,7 @@ func (l *UserDataCli) checkTransactionAccount(c *cli.Context, transaction *model
}
if relatedAccount.ParentAccountId == models.LevelOneAccountParentId && accountHasChild[relatedAccount.AccountId] {
log.BootErrorf("[user_data.checkTransactionAccount] the related account \"id:%d\" of transaction \"id:%d\" is not a sub-account", transaction.RelatedAccountId, transaction.TransactionId)
log.BootErrorf("[user_data.checkTransactionAccount] the related account \"id:%d\" of transaction \"id:%d\" is not a sub account", transaction.RelatedAccountId, transaction.TransactionId)
return errs.ErrOperationFailed
}
}
@@ -770,15 +683,15 @@ func (l *UserDataCli) checkTransactionCategory(c *cli.Context, transaction *mode
return nil
}
func (l *UserDataCli) checkTransactionTag(c *cli.Context, transactionId int64, allTagIndexesMap map[int64][]int64, tagMap map[int64]*models.TransactionTag) error {
tagIndexes, exists := allTagIndexesMap[transactionId]
func (l *UserDataCli) checkTransactionTag(c *cli.Context, transactionId int64, allTagIndexs map[int64][]int64, tagMap map[int64]*models.TransactionTag) error {
tagIndexs, exists := allTagIndexs[transactionId]
if !exists {
return nil
}
for i := 0; i < len(tagIndexes); i++ {
tagIndex := tagIndexes[i]
for i := 0; i < len(tagIndexs); i++ {
tagIndex := tagIndexs[i]
tag, exists := tagMap[tagIndex]
if !exists {
+3 -1
View File
@@ -1,11 +1,13 @@
package converters
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// DataConverter defines the structure of data exporter
type DataConverter interface {
// ToExportedContent returns the exported data
ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error)
ToExportedContent(uid int64, timezone *time.Location, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexs map[int64][]int64) ([]byte, error)
}
+168 -6
View File
@@ -1,17 +1,179 @@
package converters
import (
"fmt"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// EzBookKeepingCSVFileExporter defines the structure of CSV file exporter
// EzBookKeepingCSVFileExporter defines the structure of csv file exporter
type EzBookKeepingCSVFileExporter struct {
EzBookKeepingPlainFileExporter
DataConverter
}
const csvSeparator = ","
const csvHeaderLine = "Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Tags,Comment\n"
const csvDataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n"
// ToExportedContent returns the exported CSV data
func (e *EzBookKeepingCSVFileExporter) ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
return e.toExportedContent(uid, csvSeparator, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
// ToExportedContent returns the exported csv data
func (e *EzBookKeepingCSVFileExporter) ToExportedContent(uid int64, timezone *time.Location, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexs map[int64][]int64) ([]byte, error) {
var ret strings.Builder
ret.Grow(len(transactions) * 100)
ret.WriteString(csvHeaderLine)
for i := 0; i < len(transactions); i++ {
transaction := transactions[i]
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
continue
}
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
transactionTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
transactionTimezone := utils.FormatTimezoneOffset(transactionTimeZone)
transactionType := e.getTransactionTypeName(transaction.Type)
category := e.getTransactionCategoryName(transaction.CategoryId, categoryMap)
subCategory := e.getTransactionSubCategoryName(transaction.CategoryId, categoryMap)
account := e.getAccountName(transaction.AccountId, accountMap)
accountCurrency := e.getAccountCurrency(transaction.AccountId, accountMap)
amount := e.getDisplayAmount(transaction.Amount)
account2 := ""
account2Currency := ""
account2Amount := ""
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
account2 = e.getAccountName(transaction.RelatedAccountId, accountMap)
account2Currency = e.getAccountCurrency(transaction.RelatedAccountId, accountMap)
account2Amount = e.getDisplayAmount(transaction.RelatedAccountAmount)
}
tags := e.getTags(transaction.TransactionId, allTagIndexs, tagMap)
comment := e.getComment(transaction.Comment)
ret.WriteString(fmt.Sprintf(csvDataLineFormat, transactionTime, transactionTimezone, transactionType, category, subCategory, account, accountCurrency, amount, account2, account2Currency, account2Amount, tags, comment))
}
return []byte(ret.String()), nil
}
func (e *EzBookKeepingCSVFileExporter) getTransactionTypeName(transactionDbType models.TransactionDbType) string {
if transactionDbType == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
return "Balance Modification"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_INCOME {
return "Income"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_EXPENSE {
return "Expense"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
return "Transfer"
} else {
return ""
}
}
func (e *EzBookKeepingCSVFileExporter) getTransactionCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if !exists {
return ""
}
if category.ParentCategoryId == 0 {
return category.Name
}
parentCategory, exists := categoryMap[category.ParentCategoryId]
if !exists {
return ""
}
return parentCategory.Name
}
func (e *EzBookKeepingCSVFileExporter) getTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if exists {
return category.Name
} else {
return ""
}
}
func (e *EzBookKeepingCSVFileExporter) getAccountName(accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
return account.Name
} else {
return ""
}
}
func (e *EzBookKeepingCSVFileExporter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
return account.Currency
} else {
return ""
}
}
func (e *EzBookKeepingCSVFileExporter) getDisplayAmount(amount int64) string {
displayAmount := utils.Int64ToString(amount)
integer := utils.SubString(displayAmount, 0, len(displayAmount)-2)
decimals := utils.SubString(displayAmount, -2, 2)
if integer == "" {
integer = "0"
} else if integer == "-" {
integer = "-0"
}
if len(decimals) == 0 {
decimals = "00"
} else if len(decimals) == 1 {
decimals = "0" + decimals
}
return integer + "." + decimals
}
func (e *EzBookKeepingCSVFileExporter) getTags(transactionId int64, allTagIndexs map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
tagIndexs, exists := allTagIndexs[transactionId]
if !exists {
return ""
}
var ret strings.Builder
for i := 0; i < len(tagIndexs); i++ {
if i > 0 {
ret.WriteString(";")
}
tagIndex := tagIndexs[i]
tag, exists := tagMap[tagIndex]
if !exists {
continue
}
ret.WriteString(tag.Name)
}
return ret.String()
}
func (e *EzBookKeepingCSVFileExporter) getComment(comment string) string {
comment = strings.Replace(comment, ",", " ", -1)
comment = strings.Replace(comment, "\r\n", " ", -1)
comment = strings.Replace(comment, "\n", " ", -1)
return comment
}
-195
View File
@@ -1,195 +0,0 @@
package converters
import (
"fmt"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// EzBookKeepingPlainFileExporter defines the structure of plain file exporter
type EzBookKeepingPlainFileExporter struct {
}
const lineSeparator = "\n"
const geoLocationSeparator = " "
const transactionTagSeparator = ";"
const headerLine = "Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description" + lineSeparator
const dataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" + lineSeparator
// toExportedContent returns the exported plain data
func (e *EzBookKeepingPlainFileExporter) toExportedContent(uid int64, separator string, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
var ret strings.Builder
ret.Grow(len(transactions) * 100)
actualHeaderLine := headerLine
actualDataLineFormat := dataLineFormat
if separator != "," {
actualHeaderLine = strings.Replace(headerLine, ",", separator, -1)
actualDataLineFormat = strings.Replace(dataLineFormat, ",", separator, -1)
}
ret.WriteString(actualHeaderLine)
for i := 0; i < len(transactions); i++ {
transaction := transactions[i]
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
continue
}
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
transactionTime := utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
transactionTimezone := utils.FormatTimezoneOffset(transactionTimeZone)
transactionType := e.getTransactionTypeName(transaction.Type)
category := e.replaceDelimiters(e.getTransactionCategoryName(transaction.CategoryId, categoryMap), separator)
subCategory := e.replaceDelimiters(e.getTransactionSubCategoryName(transaction.CategoryId, categoryMap), separator)
account := e.replaceDelimiters(e.getAccountName(transaction.AccountId, accountMap), separator)
accountCurrency := e.getAccountCurrency(transaction.AccountId, accountMap)
amount := e.getDisplayAmount(transaction.Amount)
account2 := ""
account2Currency := ""
account2Amount := ""
geoLocation := ""
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
account2 = e.replaceDelimiters(e.getAccountName(transaction.RelatedAccountId, accountMap), separator)
account2Currency = e.getAccountCurrency(transaction.RelatedAccountId, accountMap)
account2Amount = e.getDisplayAmount(transaction.RelatedAccountAmount)
}
if transaction.GeoLongitude != 0 || transaction.GeoLatitude != 0 {
geoLocation = fmt.Sprintf("%f%s%f", transaction.GeoLongitude, geoLocationSeparator, transaction.GeoLatitude)
}
tags := e.replaceDelimiters(e.getTags(transaction.TransactionId, allTagIndexes, tagMap), separator)
comment := e.replaceDelimiters(transaction.Comment, separator)
ret.WriteString(fmt.Sprintf(actualDataLineFormat, transactionTime, transactionTimezone, transactionType, category, subCategory, account, accountCurrency, amount, account2, account2Currency, account2Amount, geoLocation, tags, comment))
}
return []byte(ret.String()), nil
}
func (e *EzBookKeepingPlainFileExporter) getTransactionTypeName(transactionDbType models.TransactionDbType) string {
if transactionDbType == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
return "Balance Modification"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_INCOME {
return "Income"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_EXPENSE {
return "Expense"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
return "Transfer"
} else {
return ""
}
}
func (e *EzBookKeepingPlainFileExporter) getTransactionCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if !exists {
return ""
}
if category.ParentCategoryId == 0 {
return category.Name
}
parentCategory, exists := categoryMap[category.ParentCategoryId]
if !exists {
return ""
}
return parentCategory.Name
}
func (e *EzBookKeepingPlainFileExporter) getTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if exists {
return category.Name
} else {
return ""
}
}
func (e *EzBookKeepingPlainFileExporter) getAccountName(accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
return account.Name
} else {
return ""
}
}
func (e *EzBookKeepingPlainFileExporter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
return account.Currency
} else {
return ""
}
}
func (e *EzBookKeepingPlainFileExporter) getDisplayAmount(amount int64) string {
displayAmount := utils.Int64ToString(amount)
integer := utils.SubString(displayAmount, 0, len(displayAmount)-2)
decimals := utils.SubString(displayAmount, -2, 2)
if integer == "" {
integer = "0"
} else if integer == "-" {
integer = "-0"
}
if len(decimals) == 0 {
decimals = "00"
} else if len(decimals) == 1 {
decimals = "0" + decimals
}
return integer + "." + decimals
}
func (e *EzBookKeepingPlainFileExporter) getTags(transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
tagIndexes, exists := allTagIndexes[transactionId]
if !exists {
return ""
}
var ret strings.Builder
for i := 0; i < len(tagIndexes); i++ {
if i > 0 {
ret.WriteString(transactionTagSeparator)
}
tagIndex := tagIndexes[i]
tag, exists := tagMap[tagIndex]
if !exists {
continue
}
ret.WriteString(tag.Name)
}
return ret.String()
}
func (e *EzBookKeepingPlainFileExporter) replaceDelimiters(text string, separator string) string {
text = strings.Replace(text, separator, " ", -1)
text = strings.Replace(text, "\r\n", " ", -1)
text = strings.Replace(text, "\n", " ", -1)
return text
}
-17
View File
@@ -1,17 +0,0 @@
package converters
import (
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// EzBookKeepingTSVFileExporter defines the structure of TSV file exporter
type EzBookKeepingTSVFileExporter struct {
EzBookKeepingPlainFileExporter
}
const tsvSeparator = "\t"
// ToExportedContent returns the exported TSV data
func (e *EzBookKeepingTSVFileExporter) ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
return e.toExportedContent(uid, tsvSeparator, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
}
-34
View File
@@ -1,7 +1,6 @@
package core
import (
"net"
"strconv"
"github.com/gin-gonic/gin"
@@ -17,9 +16,6 @@ const responseErrorFieldKey = "RESPONSE_ERROR"
// AcceptLanguageHeaderName represents the header name of accept language
const AcceptLanguageHeaderName = "Accept-Language"
// RemoteClientPortHeader represents the header name of remote client source port
const RemoteClientPortHeader = "X-Real-Port"
// ClientTimezoneOffsetHeaderName represents the header name of client timezone offset
const ClientTimezoneOffsetHeaderName = "X-Timezone-Offset"
@@ -29,36 +25,6 @@ type Context struct {
// DO NOT ADD ANY FIELD IN THIS CONTEXT, THIS CONTEXT IS JUST A WRAPPER
}
func (c *Context) ClientPort() uint16 {
remotePort := c.GetHeader(RemoteClientPortHeader)
if remotePort != "" {
remotePortNum, err := strconv.ParseInt(remotePort, 10, 32)
if err == nil {
return uint16(remotePortNum)
}
}
if c.Request == nil {
return 0
}
_, remotePort, err := net.SplitHostPort(c.Request.RemoteAddr)
if err != nil {
return 0
}
remotePortNum, err := strconv.ParseInt(remotePort, 10, 32)
if err != nil {
return 0
}
return uint16(remotePortNum)
}
// SetRequestId sets the given request id to context
func (c *Context) SetRequestId(requestId string) {
c.Set(requestIdFieldKey, requestId)
+1 -4
View File
@@ -12,11 +12,8 @@ type MiddlewareHandlerFunc func(*Context)
// ApiHandlerFunc represents the api handler function
type ApiHandlerFunc func(*Context) (any, *errs.Error)
// DataHandlerFunc represents the handler function that returns file data byte array and file name
// DataHandlerFunc represents the handler function that returns byte array
type DataHandlerFunc func(*Context) ([]byte, string, *errs.Error)
// ImageHandlerFunc represents the handler function that returns image byte array and content type
type ImageHandlerFunc func(*Context) ([]byte, string, *errs.Error)
// ProxyHandlerFunc represents the reverse proxy handler function
type ProxyHandlerFunc func(*Context) (*httputil.ReverseProxy, *errs.Error)
-95
View File
@@ -1,95 +0,0 @@
package core
import (
"fmt"
)
// DecimalSeparator represents the type of decimal separator
type DecimalSeparator byte
// Decimal Separator
const (
DECIMAL_SEPARATOR_DEFAULT DecimalSeparator = 0
DECIMAL_SEPARATOR_DOT DecimalSeparator = 1
DECIMAL_SEPARATOR_COMMA DecimalSeparator = 2
DECIMAL_SEPARATOR_SPACE DecimalSeparator = 3
DECIMAL_SEPARATOR_INVALID DecimalSeparator = 255
)
// String returns a textual representation of the decimal separator enum
func (f DecimalSeparator) String() string {
switch f {
case DECIMAL_SEPARATOR_DEFAULT:
return "Default"
case DECIMAL_SEPARATOR_DOT:
return "Dot"
case DECIMAL_SEPARATOR_COMMA:
return "Comma"
case DECIMAL_SEPARATOR_SPACE:
return "Space"
case DECIMAL_SEPARATOR_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
// DigitGroupingSymbol represents the digit grouping symbol
type DigitGroupingSymbol byte
// Digit Grouping Symbol
const (
DIGIT_GROUPING_SYMBOL_DEFAULT DigitGroupingSymbol = 0
DIGIT_GROUPING_SYMBOL_DOT DigitGroupingSymbol = 1
DIGIT_GROUPING_SYMBOL_COMMA DigitGroupingSymbol = 2
DIGIT_GROUPING_SYMBOL_SPACE DigitGroupingSymbol = 3
DIGIT_GROUPING_SYMBOL_APOSTROPHE DigitGroupingSymbol = 4
DIGIT_GROUPING_SYMBOL_INVALID DigitGroupingSymbol = 255
)
// String returns a textual representation of the digit grouping symbol enum
func (f DigitGroupingSymbol) String() string {
switch f {
case DIGIT_GROUPING_SYMBOL_DEFAULT:
return "Default"
case DIGIT_GROUPING_SYMBOL_DOT:
return "Dot"
case DIGIT_GROUPING_SYMBOL_COMMA:
return "Comma"
case DIGIT_GROUPING_SYMBOL_SPACE:
return "Space"
case DIGIT_GROUPING_SYMBOL_APOSTROPHE:
return "Apostrophe"
case DIGIT_GROUPING_SYMBOL_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
// DigitGroupingType represents digit grouping type
type DigitGroupingType byte
// Digit Grouping Type
const (
DIGIT_GROUPING_TYPE_DEFAULT DigitGroupingType = 0
DIGIT_GROUPING_TYPE_NONE DigitGroupingType = 1
DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR DigitGroupingType = 2
DIGIT_GROUPING_TYPE_INVALID DigitGroupingType = 255
)
// String returns a textual representation of the digit grouping type enum
func (d DigitGroupingType) String() string {
switch d {
case DIGIT_GROUPING_TYPE_DEFAULT:
return "Default"
case DIGIT_GROUPING_TYPE_NONE:
return "None"
case DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR:
return "Thousands Separator"
case DIGIT_GROUPING_TYPE_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(d))
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ func (c *XOrmContextAdapter) Err() error {
// if no value is associated with key.
func (c *XOrmContextAdapter) Value(key any) any {
if key == log.SessionIDKey && c.requestId != "" {
return fmt.Sprintf("%s", c.requestId)
return fmt.Sprintf("r=%s", c.requestId)
}
return nil
@@ -1,7 +0,0 @@
package duplicatechecker
// DuplicateChecker is common duplicate checker interface
type DuplicateChecker interface {
Get(checkerType DuplicateCheckerType, uid int64, identification string) (bool, string)
Set(checkerType DuplicateCheckerType, uid int64, identification string, remark string)
}
@@ -1,38 +0,0 @@
package duplicatechecker
import (
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// DuplicateCheckerContainer contains the current duplicate checker
type DuplicateCheckerContainer struct {
Current DuplicateChecker
}
// Initialize a duplicate checker container singleton instance
var (
Container = &DuplicateCheckerContainer{}
)
// InitializeDuplicateChecker initializes the current duplicate checker according to the config
func InitializeDuplicateChecker(config *settings.Config) error {
if config.DuplicateCheckerType == settings.InMemoryDuplicateCheckerType {
checker, err := NewInMemoryDuplicateChecker(config)
Container.Current = checker
return err
}
return errs.ErrInvalidDuplicateCheckerType
}
// Get returns whether the same submission has been processed and related remark by the current duplicate checker
func (c *DuplicateCheckerContainer) Get(checkerType DuplicateCheckerType, uid int64, identification string) (bool, string) {
return c.Current.Get(checkerType, uid, identification)
}
// Set saves the identification and remark to in-memory cache by the current duplicate checker
func (c *DuplicateCheckerContainer) Set(checkerType DuplicateCheckerType, uid int64, identification string, remark string) {
c.Current.Set(checkerType, uid, identification, remark)
}
@@ -1,13 +0,0 @@
package duplicatechecker
// DuplicateCheckerType represents duplicate checker type
type DuplicateCheckerType uint8
// Types of uuid
const (
DUPLICATE_CHECKER_TYPE_DEFAULT DuplicateCheckerType = 0
DUPLICATE_CHECKER_TYPE_NEW_ACCOUNT DuplicateCheckerType = 1
DUPLICATE_CHECKER_TYPE_NEW_CATEGORY DuplicateCheckerType = 2
DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION DuplicateCheckerType = 3
DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE DuplicateCheckerType = 4
)
@@ -1,43 +0,0 @@
package duplicatechecker
import (
"fmt"
"github.com/patrickmn/go-cache"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// InMemoryDuplicateChecker represents in-memory duplicate checker
type InMemoryDuplicateChecker struct {
cache *cache.Cache
}
// NewInMemoryDuplicateChecker returns a new in-memory duplicate checker
func NewInMemoryDuplicateChecker(config *settings.Config) (*InMemoryDuplicateChecker, error) {
checker := &InMemoryDuplicateChecker{
cache: cache.New(config.DuplicateSubmissionsIntervalDuration, config.InMemoryDuplicateCheckerCleanupIntervalDuration),
}
return checker, nil
}
// Get returns whether the same submission has been processed and related remark
func (c *InMemoryDuplicateChecker) Get(checkerType DuplicateCheckerType, uid int64, identification string) (bool, string) {
existedRemark, found := c.cache.Get(c.getCacheKey(checkerType, uid, identification))
if found {
return true, existedRemark.(string)
}
return false, ""
}
// Set saves the identification and remark to in-memory cache
func (c *InMemoryDuplicateChecker) Set(checkerType DuplicateCheckerType, uid int64, identification string, remark string) {
c.cache.Set(c.getCacheKey(checkerType, uid, identification), remark, cache.DefaultExpiration)
}
func (c *InMemoryDuplicateChecker) getCacheKey(checkerType DuplicateCheckerType, uid int64, identification string) string {
return fmt.Sprintf("%d|%d|%s", checkerType, uid, identification)
}
+5 -6
View File
@@ -8,15 +8,14 @@ var (
ErrAccountNotFound = NewNormalError(NormalSubcategoryAccount, 1, http.StatusBadRequest, "account not found")
ErrAccountTypeInvalid = NewNormalError(NormalSubcategoryAccount, 2, http.StatusBadRequest, "account type is invalid")
ErrAccountCurrencyInvalid = NewNormalError(NormalSubcategoryAccount, 3, http.StatusBadRequest, "account currency is invalid")
ErrAccountHaveNoSubAccount = NewNormalError(NormalSubcategoryAccount, 4, http.StatusBadRequest, "account must have at least one sub-account")
ErrAccountCannotHaveSubAccounts = NewNormalError(NormalSubcategoryAccount, 5, http.StatusBadRequest, "account cannot have sub-accounts")
ErrAccountHaveNoSubAccount = NewNormalError(NormalSubcategoryAccount, 4, http.StatusBadRequest, "account must have at least one sub account")
ErrAccountCannotHaveSubAccounts = NewNormalError(NormalSubcategoryAccount, 5, http.StatusBadRequest, "account cannot have sub accounts")
ErrParentAccountCannotSetCurrency = NewNormalError(NormalSubcategoryAccount, 6, http.StatusBadRequest, "parent account cannot set currency")
ErrParentAccountCannotSetBalance = NewNormalError(NormalSubcategoryAccount, 7, http.StatusBadRequest, "parent account cannot set balance")
ErrSubAccountCategoryNotEqualsToParent = NewNormalError(NormalSubcategoryAccount, 8, http.StatusBadRequest, "sub-account category not equals to parent")
ErrSubAccountTypeInvalid = NewNormalError(NormalSubcategoryAccount, 9, http.StatusBadRequest, "sub-account type invalid")
ErrCannotAddOrDeleteSubAccountsWhenModify = NewNormalError(NormalSubcategoryAccount, 10, http.StatusBadRequest, "cannot add or delete sub-accounts when modify account")
ErrSubAccountCategoryNotEqualsToParent = NewNormalError(NormalSubcategoryAccount, 8, http.StatusBadRequest, "sub account category not equals to parent")
ErrSubAccountTypeInvalid = NewNormalError(NormalSubcategoryAccount, 9, http.StatusBadRequest, "sub account type invalid")
ErrCannotAddOrDeleteSubAccountsWhenModify = NewNormalError(NormalSubcategoryAccount, 10, http.StatusBadRequest, "cannot add or delete sub accounts when modify account")
ErrSourceAccountNotFound = NewNormalError(NormalSubcategoryAccount, 11, http.StatusBadRequest, "source account not found")
ErrDestinationAccountNotFound = NewNormalError(NormalSubcategoryAccount, 12, http.StatusBadRequest, "destination account not found")
ErrAccountInUseCannotBeDeleted = NewNormalError(NormalSubcategoryAccount, 13, http.StatusBadRequest, "account is in use and cannot be deleted")
ErrAccountCategoryInvalid = NewNormalError(NormalSubcategoryAccount, 14, http.StatusBadRequest, "account category is invalid")
)
-12
View File
@@ -15,7 +15,6 @@ const (
SystemSubcategorySetting = 1
SystemSubcategoryDatabase = 2
SystemSubcategoryMail = 3
SystemSubcategoryLogging = 4
)
// Sub categories of normal error
@@ -29,8 +28,6 @@ const (
NormalSubcategoryCategory = 6
NormalSubcategoryTag = 7
NormalSubcategoryDataManagement = 8
NormalSubcategoryMapProxy = 9
NormalSubcategoryTemplate = 10
)
// Error represents the specific error returned to user
@@ -76,15 +73,6 @@ func NewNormalError(subCategory int32, index int32, httpStatusCode int, message
return New(CATEGORY_NORMAL, subCategory, index, httpStatusCode, message)
}
// NewLoggingError returns a new logging error instance
func NewLoggingError(message string, err ...error) *Error {
return New(ErrLoggingError.Category,
ErrLoggingError.SubCategory,
ErrLoggingError.Index,
ErrLoggingError.HttpStatusCode,
message, err...)
}
// NewIncompleteOrIncorrectSubmissionError returns a new incomplete or incorrect submission error instance
func NewIncompleteOrIncorrectSubmissionError(err error) *Error {
return New(ErrIncompleteOrIncorrectSubmission.Category,
+1 -6
View File
@@ -16,7 +16,7 @@ var (
ErrPageIndexInvalid = NewNormalError(NormalSubcategoryGlobal, 6, http.StatusBadRequest, "page index is invalid")
ErrPageCountInvalid = NewNormalError(NormalSubcategoryGlobal, 7, http.StatusBadRequest, "page count is invalid")
ErrClientTimezoneOffsetInvalid = NewNormalError(NormalSubcategoryGlobal, 8, http.StatusBadRequest, "client timezone offset is invalid")
ErrQueryItemsEmpty = NewNormalError(NormalSubcategoryGlobal, 9, http.StatusBadRequest, "query items cannot be blank")
ErrQueryItemsEmpty = NewNormalError(NormalSubcategoryGlobal, 9, http.StatusBadRequest, "query items cannot be empty")
ErrQueryItemsTooMuch = NewNormalError(NormalSubcategoryGlobal, 10, http.StatusBadRequest, "query items too much")
ErrQueryItemsInvalid = NewNormalError(NormalSubcategoryGlobal, 11, http.StatusBadRequest, "query items have invalid item")
ErrParameterInvalid = NewNormalError(NormalSubcategoryGlobal, 12, http.StatusBadRequest, "parameter invalid")
@@ -82,8 +82,3 @@ func GetParameterInvalidCurrencyMessage(field string) string {
func GetParameterInvalidHexRGBColorMessage(field string) string {
return fmt.Sprintf("parameter \"%s\" is invalid color", field)
}
// GetParameterInvalidAmountFilterMessage returns specific error message for invalid amount filter parameter error
func GetParameterInvalidAmountFilterMessage(field string) string {
return fmt.Sprintf("parameter \"%s\" is invalid amount filter", field)
}
-10
View File
@@ -1,10 +0,0 @@
package errs
import (
"net/http"
)
// Error codes related to logging
var (
ErrLoggingError = NewSystemError(SystemSubcategoryLogging, 0, http.StatusInternalServerError, "logging error")
)
-9
View File
@@ -1,9 +0,0 @@
package errs
import "net/http"
// Error codes related to map image proxy
var (
ErrMapProviderNotCurrent = NewNormalError(NormalSubcategoryMapProxy, 0, http.StatusBadRequest, "specified map provider is not set")
ErrImageExtensionNotSupported = NewNormalError(NormalSubcategoryMapProxy, 0, http.StatusNotFound, "specified image extension is not supported")
)
+7 -19
View File
@@ -4,23 +4,11 @@ import "net/http"
// Error codes related to settings
var (
ErrInvalidServerMode = NewSystemError(SystemSubcategorySetting, 0, http.StatusInternalServerError, "invalid server mode")
ErrInvalidProtocol = NewSystemError(SystemSubcategorySetting, 1, http.StatusInternalServerError, "invalid server protocol")
ErrInvalidLogMode = NewSystemError(SystemSubcategorySetting, 2, http.StatusInternalServerError, "invalid log mode")
ErrInvalidLogLevel = NewSystemError(SystemSubcategorySetting, 3, http.StatusInternalServerError, "invalid log level")
ErrGettingLocalAddress = NewSystemError(SystemSubcategorySetting, 4, http.StatusInternalServerError, "failed to get local address")
ErrInvalidStorageType = NewSystemError(SystemSubcategorySetting, 5, http.StatusInternalServerError, "invalid storage type")
ErrInvalidLocalFileSystemStoragePath = NewSystemError(SystemSubcategorySetting, 6, http.StatusInternalServerError, "invalid local file system storage path")
ErrInvalidUuidMode = NewSystemError(SystemSubcategorySetting, 7, http.StatusInternalServerError, "invalid uuid mode")
ErrInvalidDuplicateCheckerType = NewSystemError(SystemSubcategorySetting, 8, http.StatusInternalServerError, "invalid duplicate checker type")
ErrInvalidInMemoryDuplicateCheckerCleanupInterval = NewSystemError(SystemSubcategorySetting, 9, http.StatusInternalServerError, "invalid in-memory duplicate checker cleanup interval")
ErrInvalidTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 10, http.StatusInternalServerError, "invalid token expired time")
ErrInvalidTokenMinRefreshInterval = NewSystemError(SystemSubcategorySetting, 11, http.StatusInternalServerError, "invalid token min refresh interval")
ErrInvalidTemporaryTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 12, http.StatusInternalServerError, "invalid temporary token expired time")
ErrInvalidEmailVerifyTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 13, http.StatusInternalServerError, "invalid email verify token expired time")
ErrInvalidAvatarProvider = NewSystemError(SystemSubcategorySetting, 14, http.StatusInternalServerError, "invalid avatar provider")
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 15, http.StatusInternalServerError, "invalid map provider")
ErrInvalidAmapSecurityVerificationMethod = NewSystemError(SystemSubcategorySetting, 16, http.StatusInternalServerError, "invalid amap security verification method")
ErrInvalidPasswordResetTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 17, http.StatusInternalServerError, "invalid password reset token expired time")
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 18, http.StatusInternalServerError, "invalid exchange rates data source")
ErrInvalidProtocol = NewSystemError(SystemSubcategorySetting, 0, http.StatusInternalServerError, "invalid server protocol")
ErrInvalidLogMode = NewSystemError(SystemSubcategorySetting, 1, http.StatusInternalServerError, "invalid log mode")
ErrGettingLocalAddress = NewSystemError(SystemSubcategorySetting, 2, http.StatusInternalServerError, "failed to get local address")
ErrInvalidUuidMode = NewSystemError(SystemSubcategorySetting, 3, http.StatusInternalServerError, "invalid uuid mode")
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 4, http.StatusInternalServerError, "invalid exchange rates data source")
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 5, http.StatusInternalServerError, "invalid map provider")
ErrInvalidAmapSecurityVerificationMethod = NewSystemError(SystemSubcategorySetting, 6, http.StatusInternalServerError, "invalid amap security verification method")
)
+5 -7
View File
@@ -4,11 +4,9 @@ import "net/http"
// Error codes related to transaction categories
var (
ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error")
ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found")
ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed")
ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented")
ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusServiceUnavailable, "system is busy")
ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported")
ErrImageTypeNotSupported = NewSystemError(SystemSubcategoryDefault, 6, http.StatusBadRequest, "image type not supported")
ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error")
ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found")
ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed")
ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented")
ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusNotImplemented, "system is busy")
)
+2 -2
View File
@@ -11,8 +11,8 @@ var (
ErrCurrentInvalidToken = NewNormalError(NormalSubcategoryToken, 2, http.StatusUnauthorized, "current token is invalid")
ErrCurrentTokenExpired = NewNormalError(NormalSubcategoryToken, 3, http.StatusUnauthorized, "current token is expired")
ErrCurrentInvalidTokenType = NewNormalError(NormalSubcategoryToken, 4, http.StatusUnauthorized, "current token type is invalid")
ErrCurrentTokenRequire2FA = NewNormalError(NormalSubcategoryToken, 5, http.StatusUnauthorized, "current token requires two-factor authorization")
ErrCurrentTokenNotRequire2FA = NewNormalError(NormalSubcategoryToken, 6, http.StatusUnauthorized, "current token does not require two-factor authorization")
ErrCurrentTokenRequire2FA = NewNormalError(NormalSubcategoryToken, 5, http.StatusUnauthorized, "current token requires two factor authorization")
ErrCurrentTokenNotRequire2FA = NewNormalError(NormalSubcategoryToken, 6, http.StatusUnauthorized, "current token does not require two factor authorization")
ErrInvalidToken = NewNormalError(NormalSubcategoryToken, 7, http.StatusBadRequest, "token is invalid")
ErrInvalidTokenId = NewNormalError(NormalSubcategoryToken, 8, http.StatusBadRequest, "token id is invalid")
ErrInvalidUserTokenId = NewNormalError(NormalSubcategoryToken, 9, http.StatusBadRequest, "user token id is invalid")
+3 -9
View File
@@ -18,13 +18,7 @@ var (
ErrCannotAddTransactionToHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 11, http.StatusBadRequest, "cannot add transaction to hidden account")
ErrCannotModifyTransactionInHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 12, http.StatusBadRequest, "cannot modify transaction of hidden account")
ErrCannotDeleteTransactionInHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 13, http.StatusBadRequest, "cannot delete transaction in hidden account")
ErrCannotAddTransactionToParentAccount = NewNormalError(NormalSubcategoryTransaction, 14, http.StatusBadRequest, "cannot add transaction to parent account")
ErrCannotModifyTransactionInParentAccount = NewNormalError(NormalSubcategoryTransaction, 15, http.StatusBadRequest, "cannot modify transaction of parent account")
ErrCannotDeleteTransactionInParentAccount = NewNormalError(NormalSubcategoryTransaction, 16, http.StatusBadRequest, "cannot delete transaction in parent account")
ErrCannotCreateTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 17, http.StatusBadRequest, "cannot add transaction with this transaction time")
ErrCannotModifyTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 18, http.StatusBadRequest, "cannot modify transaction with this transaction time")
ErrCannotDeleteTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 19, http.StatusBadRequest, "cannot delete transaction with this transaction time")
ErrCannotUseHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 20, http.StatusBadRequest, "cannot use hidden account")
ErrCannotUseHiddenTransactionCategory = NewNormalError(NormalSubcategoryTransaction, 21, http.StatusBadRequest, "cannot use hidden transaction category")
ErrCannotUseHiddenTransactionTag = NewNormalError(NormalSubcategoryTransaction, 22, http.StatusBadRequest, "cannot use hidden transaction tag")
ErrCannotCreateTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 14, http.StatusBadRequest, "cannot add transaction with this transaction time")
ErrCannotModifyTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 15, http.StatusBadRequest, "cannot modify transaction with this transaction time")
ErrCannotDeleteTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 16, http.StatusBadRequest, "cannot delete transaction with this transaction time")
)
+7 -11
View File
@@ -4,15 +4,11 @@ import "net/http"
// Error codes related to transaction categories
var (
ErrTransactionCategoryIdInvalid = NewNormalError(NormalSubcategoryCategory, 0, http.StatusBadRequest, "transaction category id is invalid")
ErrTransactionCategoryNotFound = NewNormalError(NormalSubcategoryCategory, 1, http.StatusBadRequest, "transaction category not found")
ErrTransactionCategoryTypeInvalid = NewNormalError(NormalSubcategoryCategory, 2, http.StatusBadRequest, "transaction category type is invalid")
ErrParentTransactionCategoryNotFound = NewNormalError(NormalSubcategoryCategory, 3, http.StatusBadRequest, "parent transaction category not found")
ErrCannotAddToSecondaryTransactionCategory = NewNormalError(NormalSubcategoryCategory, 4, http.StatusBadRequest, "cannot add to secondary transaction category")
ErrCannotUsePrimaryCategoryForTransaction = NewNormalError(NormalSubcategoryCategory, 5, http.StatusBadRequest, "cannot use primary category for transaction category")
ErrTransactionCategoryInUseCannotBeDeleted = NewNormalError(NormalSubcategoryCategory, 6, http.StatusBadRequest, "transaction category is in use and cannot be deleted")
ErrNotAllowChangePrimaryTransactionCategoryToSecondary = NewNormalError(NormalSubcategoryCategory, 7, http.StatusBadRequest, "not allow to change primary category to secondary category")
ErrNotAllowChangeSecondaryTransactionCategoryToPrimary = NewNormalError(NormalSubcategoryCategory, 8, http.StatusBadRequest, "not allow to change secondary category to primary category")
ErrNotAllowChangePrimaryTransactionType = NewNormalError(NormalSubcategoryCategory, 9, http.StatusBadRequest, "not allow to change primary category with different type")
ErrNotAllowUseSecondaryTransactionAsPrimaryCategory = NewNormalError(NormalSubcategoryCategory, 10, http.StatusBadRequest, "not allow to use secondary category as primary category")
ErrTransactionCategoryIdInvalid = NewNormalError(NormalSubcategoryCategory, 0, http.StatusBadRequest, "transaction category id is invalid")
ErrTransactionCategoryNotFound = NewNormalError(NormalSubcategoryCategory, 1, http.StatusBadRequest, "transaction category not found")
ErrTransactionCategoryTypeInvalid = NewNormalError(NormalSubcategoryCategory, 2, http.StatusBadRequest, "transaction category type is invalid")
ErrParentTransactionCategoryNotFound = NewNormalError(NormalSubcategoryCategory, 3, http.StatusBadRequest, "parent transaction category not found")
ErrCannotAddToSecondaryTransactionCategory = NewNormalError(NormalSubcategoryCategory, 4, http.StatusBadRequest, "cannot add to secondary transaction category")
ErrCannotUsePrimaryCategoryForTransaction = NewNormalError(NormalSubcategoryCategory, 5, http.StatusBadRequest, "cannot use primary category for transaction category")
ErrTransactionCategoryInUseCannotBeDeleted = NewNormalError(NormalSubcategoryCategory, 6, http.StatusBadRequest, "transaction category is in use and cannot be deleted")
)
-1
View File
@@ -9,5 +9,4 @@ var (
ErrTransactionTagNameIsEmpty = NewNormalError(NormalSubcategoryTag, 2, http.StatusBadRequest, "transaction tag name is empty")
ErrTransactionTagNameAlreadyExists = NewNormalError(NormalSubcategoryTag, 3, http.StatusBadRequest, "transaction tag name already exists")
ErrTransactionTagInUseCannotBeDeleted = NewNormalError(NormalSubcategoryTag, 4, http.StatusBadRequest, "transaction tag is in use and cannot be deleted")
ErrTransactionTagIndexNotFound = NewNormalError(NormalSubcategoryTag, 5, http.StatusBadRequest, "transaction tag index not found")
)
-10
View File
@@ -1,10 +0,0 @@
package errs
import "net/http"
// Error codes related to transaction templates
var (
ErrTransactionTemplateIdInvalid = NewNormalError(NormalSubcategoryTemplate, 0, http.StatusBadRequest, "transaction template id is invalid")
ErrTransactionTemplateNotFound = NewNormalError(NormalSubcategoryTemplate, 1, http.StatusBadRequest, "transaction template not found")
ErrTransactionTemplateTypeInvalid = NewNormalError(NormalSubcategoryTemplate, 2, http.StatusBadRequest, "transaction template type is invalid")
)
+5 -5
View File
@@ -2,11 +2,11 @@ package errs
import "net/http"
// Error codes related to two-factor authorization
// Error codes related to two factor authorization
var (
ErrPasscodeInvalid = NewNormalError(NormalSubcategoryTwofactor, 0, http.StatusUnauthorized, "passcode is invalid")
ErrTwoFactorRecoveryCodeInvalid = NewNormalError(NormalSubcategoryTwofactor, 1, http.StatusUnauthorized, "two-factor backup code is invalid")
ErrTwoFactorRecoveryCodeNotExist = NewNormalError(NormalSubcategoryTwofactor, 2, http.StatusUnauthorized, "two-factor backup code does not exist")
ErrTwoFactorIsNotEnabled = NewNormalError(NormalSubcategoryTwofactor, 3, http.StatusBadRequest, "two-factor is not enabled")
ErrTwoFactorAlreadyEnabled = NewNormalError(NormalSubcategoryTwofactor, 4, http.StatusBadRequest, "two-factor has already been enabled")
ErrTwoFactorRecoveryCodeInvalid = NewNormalError(NormalSubcategoryTwofactor, 1, http.StatusUnauthorized, "two factor backup code is invalid")
ErrTwoFactorRecoveryCodeNotExist = NewNormalError(NormalSubcategoryTwofactor, 2, http.StatusUnauthorized, "two factor backup code does not exist")
ErrTwoFactorIsNotEnabled = NewNormalError(NormalSubcategoryTwofactor, 3, http.StatusBadRequest, "two factor is not enabled")
ErrTwoFactorAlreadyEnabled = NewNormalError(NormalSubcategoryTwofactor, 4, http.StatusBadRequest, "two factor has already been enabled")
)
+23 -28
View File
@@ -6,32 +6,27 @@ import (
// Error codes related to users
var (
ErrLoginNameInvalid = NewNormalError(NormalSubcategoryUser, 0, http.StatusUnauthorized, "login name is invalid")
ErrLoginNameOrPasswordInvalid = NewNormalError(NormalSubcategoryUser, 1, http.StatusUnauthorized, "login name or password is invalid")
ErrLoginNameOrPasswordWrong = NewNormalError(NormalSubcategoryUser, 2, http.StatusUnauthorized, "login name or password is wrong")
ErrUserIdInvalid = NewNormalError(NormalSubcategoryUser, 3, http.StatusBadRequest, "user id is invalid")
ErrUsernameIsEmpty = NewNormalError(NormalSubcategoryUser, 4, http.StatusBadRequest, "username is empty")
ErrEmailIsEmpty = NewNormalError(NormalSubcategoryUser, 5, http.StatusBadRequest, "email is empty")
ErrNicknameIsEmpty = NewNormalError(NormalSubcategoryUser, 6, http.StatusBadRequest, "nickname is empty")
ErrPasswordIsEmpty = NewNormalError(NormalSubcategoryUser, 7, http.StatusBadRequest, "password is empty")
ErrUserDefaultCurrencyIsEmpty = NewNormalError(NormalSubcategoryUser, 8, http.StatusBadRequest, "user default currency is empty")
ErrUserDefaultCurrencyIsInvalid = NewNormalError(NormalSubcategoryUser, 9, http.StatusBadRequest, "user default currency is invalid")
ErrUserNotFound = NewNormalError(NormalSubcategoryUser, 10, http.StatusBadRequest, "user not found")
ErrUserPasswordWrong = NewNormalError(NormalSubcategoryUser, 11, http.StatusBadRequest, "password is wrong")
ErrUsernameAlreadyExists = NewNormalError(NormalSubcategoryUser, 12, http.StatusBadRequest, "username already exists")
ErrUserEmailAlreadyExists = NewNormalError(NormalSubcategoryUser, 13, http.StatusBadRequest, "email already exists")
ErrUserRegistrationNotAllowed = NewNormalError(NormalSubcategoryUser, 14, http.StatusBadRequest, "user registration not allowed")
ErrUserDefaultAccountIsInvalid = NewNormalError(NormalSubcategoryUser, 15, http.StatusBadRequest, "user default account is invalid")
ErrUserIsDisabled = NewNormalError(NormalSubcategoryUser, 16, http.StatusBadRequest, "user is disabled")
ErrEmptyIsInvalid = NewNormalError(NormalSubcategoryUser, 17, http.StatusBadRequest, "email is invalid")
ErrEmailIsEmptyOrInvalid = NewNormalError(NormalSubcategoryUser, 18, http.StatusBadRequest, "email is empty or invalid")
ErrNewPasswordEqualsOldInvalid = NewNormalError(NormalSubcategoryUser, 19, http.StatusBadRequest, "new password equals old password")
ErrEmailIsNotVerified = NewNormalError(NormalSubcategoryUser, 20, http.StatusBadRequest, "email is not verified")
ErrEmailIsVerified = NewNormalError(NormalSubcategoryUser, 21, http.StatusBadRequest, "email is verified")
ErrEmailValidationNotAllowed = NewNormalError(NormalSubcategoryUser, 22, http.StatusBadRequest, "email validation not allowed")
ErrDecimalSeparatorAndDigitGroupingSymbolCannotBeEqual = NewNormalError(NormalSubcategoryUser, 23, http.StatusBadRequest, "decimal separator and digit grouping symbol cannot be equal")
ErrUserDefaultAccountIsHidden = NewNormalError(NormalSubcategoryUser, 24, http.StatusBadRequest, "user default account is hidden")
ErrNoUserAvatar = NewNormalError(NormalSubcategoryUser, 25, http.StatusBadRequest, "no user avatar")
ErrUserAvatarIsEmpty = NewNormalError(NormalSubcategoryUser, 26, http.StatusBadRequest, "user avatar is empty")
ErrUserAvatarNoExists = NewNormalError(NormalSubcategoryUser, 27, http.StatusNotFound, "user avatar not exists")
ErrLoginNameInvalid = NewNormalError(NormalSubcategoryUser, 0, http.StatusUnauthorized, "login name is invalid")
ErrLoginNameOrPasswordInvalid = NewNormalError(NormalSubcategoryUser, 1, http.StatusUnauthorized, "login name or password is invalid")
ErrLoginNameOrPasswordWrong = NewNormalError(NormalSubcategoryUser, 2, http.StatusUnauthorized, "login name or password is wrong")
ErrUserIdInvalid = NewNormalError(NormalSubcategoryUser, 3, http.StatusBadRequest, "user id is invalid")
ErrUsernameIsEmpty = NewNormalError(NormalSubcategoryUser, 4, http.StatusBadRequest, "username is empty")
ErrEmailIsEmpty = NewNormalError(NormalSubcategoryUser, 5, http.StatusBadRequest, "email is empty")
ErrNicknameIsEmpty = NewNormalError(NormalSubcategoryUser, 6, http.StatusBadRequest, "nickname is empty")
ErrPasswordIsEmpty = NewNormalError(NormalSubcategoryUser, 7, http.StatusBadRequest, "password is empty")
ErrUserDefaultCurrencyIsEmpty = NewNormalError(NormalSubcategoryUser, 8, http.StatusBadRequest, "user default currency is empty")
ErrUserDefaultCurrencyIsInvalid = NewNormalError(NormalSubcategoryUser, 9, http.StatusBadRequest, "user default currency is invalid")
ErrUserNotFound = NewNormalError(NormalSubcategoryUser, 10, http.StatusBadRequest, "user not found")
ErrUserPasswordWrong = NewNormalError(NormalSubcategoryUser, 11, http.StatusBadRequest, "password is wrong")
ErrUsernameAlreadyExists = NewNormalError(NormalSubcategoryUser, 12, http.StatusBadRequest, "username already exists")
ErrUserEmailAlreadyExists = NewNormalError(NormalSubcategoryUser, 13, http.StatusBadRequest, "email already exists")
ErrUserRegistrationNotAllowed = NewNormalError(NormalSubcategoryUser, 14, http.StatusBadRequest, "user registration not allowed")
ErrUserDefaultAccountIsInvalid = NewNormalError(NormalSubcategoryUser, 15, http.StatusBadRequest, "user default account is invalid")
ErrUserIsDisabled = NewNormalError(NormalSubcategoryUser, 16, http.StatusBadRequest, "user is disabled")
ErrEmptyIsInvalid = NewNormalError(NormalSubcategoryUser, 17, http.StatusBadRequest, "email is invalid")
ErrEmailIsEmptyOrInvalid = NewNormalError(NormalSubcategoryUser, 18, http.StatusBadRequest, "email is empty or invalid")
ErrNewPasswordEqualsOldInvalid = NewNormalError(NormalSubcategoryUser, 19, http.StatusBadRequest, "new password equals old password")
ErrEmailIsNotVerified = NewNormalError(NormalSubcategoryUser, 20, http.StatusBadRequest, "email is not verified")
ErrEmailIsVerified = NewNormalError(NormalSubcategoryUser, 21, http.StatusBadRequest, "email is verified")
ErrEmailValidationNotAllowed = NewNormalError(NormalSubcategoryUser, 22, http.StatusBadRequest, "email validation not allowed")
)
@@ -14,9 +14,9 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
const nationalBankOfPolandDailyExchangeRateUrl = "https://api.nbp.pl/api/exchangerates/tables/A?format=xml"
const nationalBankOfPolandInconvertibleCurrencyExchangeRateUrl = "https://api.nbp.pl/api/exchangerates/tables/B?format=xml"
const nationalBankOfPolandExchangeRateReferenceUrl = "https://nbp.pl/en/statistic-and-financial-reporting/rates/"
const nationalBankOfPolandDailyExchangeRateUrl = "https://www.nbp.pl/kursy/xml/en/lastaen.xml"
const nationalBankOfPolandInconvertibleCurrencyExchangeRateUrl = "https://www.nbp.pl/kursy/xml/en/lastben.xml"
const nationalBankOfPolandExchangeRateReferenceUrl = "https://www.nbp.pl/homen.aspx?f=/kursy/kursyen.htm"
const nationalBankOfPolandDataSource = "Narodowy Bank Polski"
const nationalBankOfPolandBaseCurrency = "PLN"
@@ -30,15 +30,16 @@ type NationalBankOfPolandDataSource struct {
// NationalBankOfPolandExchangeRateData represents the whole data from National Bank of Poland
type NationalBankOfPolandExchangeRateData struct {
XMLName xml.Name `xml:"ArrayOfExchangeRatesTable"`
Date string `xml:"ExchangeRatesTable>EffectiveDate"`
AllExchangeRates []*NationalBankOfPolandExchangeRate `xml:"ExchangeRatesTable>Rates>Rate"`
XMLName xml.Name `xml:"exchange_rates"`
Date string `xml:"date,attr"`
AllExchangeRates []*NationalBankOfPolandExchangeRate `xml:"mid-rate"`
}
// NationalBankOfPolandExchangeRate represents the exchange rate data from National Bank of Poland
type NationalBankOfPolandExchangeRate struct {
Currency string `xml:"Code"`
Rate string `xml:"Mid"`
Currency string `xml:"code,attr"`
Units string `xml:"units,attr"`
Rate string `xml:",chardata"`
}
// ToLatestExchangeRateResponse returns a view-object according to original data from National Bank of Poland
@@ -94,6 +95,13 @@ func (e *NationalBankOfPolandExchangeRateData) ToLatestExchangeRateResponse(c *c
// ToLatestExchangeRate returns a data pair according to original data from National Bank of Poland
func (e *NationalBankOfPolandExchangeRate) ToLatestExchangeRate(c *core.Context) *models.LatestExchangeRate {
amount, err := utils.StringToInt64(e.Units)
if err != nil {
log.WarnfWithRequestId(c, "[national_bank_of_poland_datasource.ToLatestExchangeRate] failed to parse amount, currency is %s, amount is %s", e.Currency, e.Units)
return nil
}
rate, err := utils.StringToFloat64(e.Rate)
if err != nil {
@@ -106,7 +114,7 @@ func (e *NationalBankOfPolandExchangeRate) ToLatestExchangeRate(c *core.Context)
return nil
}
finalRate := 1 / rate
finalRate := float64(amount) / rate
if math.IsInf(finalRate, 0) {
return nil
@@ -10,22 +10,11 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/models"
)
const nationalBankOfPolandMinimumRequiredContent = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n" +
" <ExchangeRatesTable>\n" +
" <EffectiveDate>2024-02-28</EffectiveDate>\n" +
" <Rates>\n" +
" <Rate>\n" +
" <Code>USD</Code>\n" +
" <Mid>3.9922</Mid>\n" +
" </Rate>\n" +
" <Rate>\n" +
" <Code>CNY</Code>\n" +
" <Mid>0.5545</Mid>\n" +
" </Rate>\n" +
" </Rates>\n" +
" </ExchangeRatesTable>\n" +
"</ArrayOfExchangeRatesTable>"
const nationalBankOfPolandMinimumRequiredContent = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n" +
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n" +
" <mid-rate currency=\"US Dollar\" units=\"1\" code=\"USD\">3.8986</mid-rate>\n" +
" <mid-rate currency=\"Yuan Renminbi\" units=\"1\" code=\"CNY\">0.5941</mid-rate>\n" +
"</exchange_rates>"
func TestNationalBankOfPolandDataSource_StandardDataExtractBaseCurrency(t *testing.T) {
dataSource := &NationalBankOfPolandDataSource{}
@@ -48,11 +37,11 @@ func TestNationalBankOfPolandDataSource_StandardDataExtractExchangeRates(t *test
assert.Equal(t, nil, err)
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
Currency: "USD",
Rate: "0.2504884524823406",
Rate: "0.25650233417124096",
})
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
Currency: "CNY",
Rate: "1.8034265103697025",
Rate: "1.68321831341525",
})
}
@@ -72,33 +61,7 @@ func TestNationalBankOfPolandDataSource_OnlyXMLHeader(t *testing.T) {
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>"))
assert.NotEqual(t, nil, err)
}
func TestNationalBankOfPolandDataSource_EmptyArrayOfExchangeRatesTable(t *testing.T) {
dataSource := &NationalBankOfPolandDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
"</ArrayOfExchangeRatesTable>"))
assert.NotEqual(t, nil, err)
}
func TestNationalBankOfPolandDataSource_EmptyExchangeRatesTable(t *testing.T) {
dataSource := &NationalBankOfPolandDataSource{}
context := &core.Context{
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
" <ExchangeRatesTable>\n"+
" </ExchangeRatesTable>\n"+
"</ArrayOfExchangeRatesTable>"))
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>"))
assert.NotEqual(t, nil, err)
}
@@ -108,14 +71,9 @@ func TestNationalBankOfPolandDataSource_EmptyExchangeRatesContent(t *testing.T)
Context: &gin.Context{},
}
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
" <ExchangeRatesTable>\n"+
" <EffectiveDate>2024-02-28</EffectiveDate>\n"+
" <Rates>\n"+
" </Rates>\n"+
" </ExchangeRatesTable>\n"+
"</ArrayOfExchangeRatesTable>"))
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"+
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n"+
"</exchange_rates>"))
assert.NotEqual(t, nil, err)
}
@@ -125,18 +83,10 @@ func TestNationalBankOfPolandDataSource_InvalidCurrency(t *testing.T) {
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
" <ExchangeRatesTable>\n"+
" <EffectiveDate>2024-02-28</EffectiveDate>\n"+
" <Rates>\n"+
" <Rate>\n"+
" <Code>XXX</Code>\n"+
" <Mid>1</Mid>\n"+
" </Rate>\n"+
" </Rates>\n"+
" </ExchangeRatesTable>\n"+
"</ArrayOfExchangeRatesTable>"))
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"+
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n"+
" <mid-rate currency=\"XXX\" units=\"1\" code=\"XXX\">1</mid-rate>\n"+
"</exchange_rates>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
@@ -147,18 +97,10 @@ func TestNationalBankOfPolandDataSource_EmptyRate(t *testing.T) {
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
" <ExchangeRatesTable>\n"+
" <EffectiveDate>2024-02-28</EffectiveDate>\n"+
" <Rates>\n"+
" <Rate>\n"+
" <Code>USD</Code>\n"+
" <Mid></Mid>\n"+
" </Rate>\n"+
" </Rates>\n"+
" </ExchangeRatesTable>\n"+
"</ArrayOfExchangeRatesTable>"))
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"+
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n"+
" <mid-rate currency=\"US Dollar\" units=\"1\" code=\"USD\"></mid-rate>\n"+
"</exchange_rates>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
@@ -169,18 +111,10 @@ func TestNationalBankOfPolandDataSource_InvalidRate(t *testing.T) {
Context: &gin.Context{},
}
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+
"<ArrayOfExchangeRatesTable xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"+
" <ExchangeRatesTable>\n"+
" <EffectiveDate>2024-02-28</EffectiveDate>\n"+
" <Rates>\n"+
" <Rate>\n"+
" <Code>USD</Code>\n"+
" <Mid>null</Mid>\n"+
" </Rate>\n"+
" </Rates>\n"+
" </ExchangeRatesTable>\n"+
"</ArrayOfExchangeRatesTable>"))
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"+
"<exchange_rates table=\"A\" date=\"2021-04-02\" number=\"064/A/NBP/2021\" uid=\"21a064\">\n"+
" <mid-rate currency=\"US Dollar\" units=\"1\" code=\"USD\">null</mid-rate>\n"+
"</exchange_rates>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
-27
View File
@@ -1,14 +1,9 @@
package locales
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
)
// DefaultLanguage represents the default language
var DefaultLanguage = en
// AllLanguages represents all the supported language
// To add new languages, please refer to https://ezbookkeeping.mayswind.net/translating
var AllLanguages = map[string]*LocaleInfo{
"en": {
Content: en,
@@ -27,25 +22,3 @@ func GetLocaleTextItems(locale string) *LocaleTextItems {
return DefaultLanguage
}
func IsDecimalSeparatorEqualsDigitGroupingSymbol(decimalSeparator core.DecimalSeparator, digitGroupingSymbol core.DigitGroupingSymbol, locale string) bool {
if decimalSeparator == core.DECIMAL_SEPARATOR_DEFAULT && digitGroupingSymbol == core.DIGIT_GROUPING_SYMBOL_DEFAULT {
return false
}
if byte(decimalSeparator) == byte(digitGroupingSymbol) {
return true
}
localeTextItems := GetLocaleTextItems(locale)
if decimalSeparator == core.DECIMAL_SEPARATOR_DEFAULT {
decimalSeparator = localeTextItems.DefaultTypes.DecimalSeparator
}
if digitGroupingSymbol == core.DIGIT_GROUPING_SYMBOL_DEFAULT {
digitGroupingSymbol = localeTextItems.DefaultTypes.DigitGroupingSymbol
}
return byte(decimalSeparator) == byte(digitGroupingSymbol)
}
-10
View File
@@ -1,21 +1,11 @@
package locales
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
)
// LocaleTextItems represents all text items need to be translated
type LocaleTextItems struct {
DefaultTypes *DefaultTypes
VerifyEmailTextItems *VerifyEmailTextItems
ForgetPasswordMailTextItems *ForgetPasswordMailTextItems
}
type DefaultTypes struct {
DecimalSeparator core.DecimalSeparator
DigitGroupingSymbol core.DigitGroupingSymbol
}
// VerifyEmailTextItems represents text items need to be translated in verify mail
type VerifyEmailTextItems struct {
Title string
-8
View File
@@ -1,14 +1,6 @@
package locales
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
)
var en = &LocaleTextItems{
DefaultTypes: &DefaultTypes{
DecimalSeparator: core.DECIMAL_SEPARATOR_DOT,
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_COMMA,
},
VerifyEmailTextItems: &VerifyEmailTextItems{
Title: "Verify Email",
SalutationFormat: "Hi %s,",
-8
View File
@@ -1,14 +1,6 @@
package locales
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
)
var zhHans = &LocaleTextItems{
DefaultTypes: &DefaultTypes{
DecimalSeparator: core.DECIMAL_SEPARATOR_DOT,
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_COMMA,
},
VerifyEmailTextItems: &VerifyEmailTextItems{
Title: "验证邮箱",
SalutationFormat: "%s 您好,",
+1 -1
View File
@@ -42,7 +42,7 @@ func (f *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
}
if requestId, exists := entry.Data[logFieldRequestId]; exists {
b.WriteString(fmt.Sprintf("[%s] ", requestId))
b.WriteString(fmt.Sprintf("[r=%s] ", requestId))
}
b.WriteString(entry.Message)
+9 -43
View File
@@ -41,71 +41,37 @@ func init() {
// SetLoggerConfiguration sets the logger according to the config
func SetLoggerConfiguration(config *settings.Config, isDisableBootLog bool) error {
var bootWriters []io.Writer
var defaultWriters []io.Writer
var requestWriters []io.Writer
var queryWriters []io.Writer
var writers []io.Writer
if !isDisableBootLog {
bootWriters = append(bootWriters, os.Stdout)
}
if config.EnableConsoleLog {
defaultWriters = append(defaultWriters, os.Stdout)
requestWriters = append(requestWriters, os.Stdout)
queryWriters = append(queryWriters, os.Stdout)
writers = append(writers, os.Stdout)
}
if config.EnableFileLog {
defaultWriter, err := NewRotateFileWriter(config.FileLogPath, config.LogFileRotate, int64(config.LogFileMaxSize), config.LogFileMaxDays)
logFile, err := os.OpenFile(config.FileLogPath, os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
return err
}
if !isDisableBootLog {
bootWriters = append(bootWriters, defaultWriter)
bootWriters = append(bootWriters, logFile)
}
defaultWriters = append(defaultWriters, defaultWriter)
if config.EnableRequestLog {
if config.RequestFileLogPath != "" && config.RequestFileLogPath != config.FileLogPath {
requestWriter, err := NewRotateFileWriter(config.RequestFileLogPath, config.LogFileRotate, int64(config.LogFileMaxSize), config.LogFileMaxDays)
if err != nil {
return err
}
requestWriters = append(requestWriters, requestWriter)
} else {
requestWriters = append(requestWriters, defaultWriter)
}
}
if config.EnableQueryLog {
if config.QueryFileLogPath != "" && config.QueryFileLogPath != config.FileLogPath {
queryWriter, err := NewRotateFileWriter(config.QueryFileLogPath, config.LogFileRotate, int64(config.LogFileMaxSize), config.LogFileMaxDays)
if err != nil {
return err
}
queryWriters = append(queryWriters, queryWriter)
} else {
queryWriters = append(queryWriters, defaultWriter)
}
}
writers = append(writers, logFile)
}
bootMultipleWriter := io.MultiWriter(bootWriters...)
defaultMultipleWriter := io.MultiWriter(defaultWriters...)
requestMultipleWriter := io.MultiWriter(requestWriters...)
queryMultipleWriter := io.MultiWriter(queryWriters...)
multipleWriter := io.MultiWriter(writers...)
bootLogger.SetOutput(bootMultipleWriter)
defaultLogger.SetOutput(defaultMultipleWriter)
requestLogger.SetOutput(requestMultipleWriter)
sqlQueryLogger.SetOutput(queryMultipleWriter)
defaultLogger.SetOutput(multipleWriter)
requestLogger.SetOutput(multipleWriter)
sqlQueryLogger.SetOutput(multipleWriter)
if config.LogLevel == settings.LOGLEVEL_DEBUG {
bootLogger.SetLevel(logrus.DebugLevel)
-195
View File
@@ -1,195 +0,0 @@
package log
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/sirupsen/logrus"
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
const (
logRotateSuffixDateFormat = "20060102150405"
)
type RotateFileWriter struct {
EnableRotate bool
MaxFileSize int64
MaxFileDays uint32
filePath string
file *os.File
totalSize int64
mutex sync.Mutex
lastRemoveOldFilesDay int
}
var logFallbackLogger = logrus.New()
func init() {
logFallbackLogger.SetFormatter(&LogFormatter{})
logFallbackLogger.SetOutput(os.Stdout)
logFallbackLogger.SetLevel(logrus.InfoLevel)
}
// NewRotateFileWriter returns a new rotate file writer
func NewRotateFileWriter(filePath string, enableRotate bool, maxFileSize int64, maxFileDays uint32) (*RotateFileWriter, error) {
writer := &RotateFileWriter{
EnableRotate: enableRotate,
MaxFileSize: maxFileSize,
MaxFileDays: maxFileDays,
filePath: filePath,
totalSize: 0,
}
err := writer.openFile()
if err != nil {
return nil, err
}
return writer, nil
}
// Write does log data to specified file
func (w *RotateFileWriter) Write(p []byte) (n int, err error) {
dataSize := int64(len(p))
if w.EnableRotate && w.totalSize > 0 && w.totalSize+dataSize >= w.MaxFileSize {
w.mutex.Lock()
defer w.mutex.Unlock()
if w.EnableRotate && w.totalSize > 0 && w.totalSize+dataSize >= w.MaxFileSize {
err := w.rotateFile()
if err != nil {
logFallbackLogger.Errorf("[rotate_file_writer.Write] cannot rotate log file \"%s\", because %s", w.file.Name(), err.Error())
return 0, err
}
}
}
writeSize, err := w.file.Write(p)
if err != nil {
return 0, err
}
w.totalSize += int64(writeSize)
if w.EnableRotate {
today := time.Now().Day()
if today != w.lastRemoveOldFilesDay && w.MaxFileDays > 0 {
w.lastRemoveOldFilesDay = today
go w.removeOldFiles()
}
}
return writeSize, err
}
func (w *RotateFileWriter) rotateFile() error {
currentFileName := w.file.Name()
err := w.file.Close()
if err != nil {
return errs.NewLoggingError(fmt.Sprintf("cannot close log file \"%s\", because %s", w.file.Name(), err.Error()), err)
}
w.file = nil
archiveFileName := fmt.Sprintf("%s.%s", currentFileName, time.Now().Format(logRotateSuffixDateFormat))
err = os.Rename(currentFileName, archiveFileName)
if err != nil {
return errs.NewLoggingError(fmt.Sprintf("cannot rename log file \"%s\" to \"%s\", because %s", currentFileName, archiveFileName, err.Error()), err)
}
err = w.openFile()
if err != nil {
return err
}
return nil
}
func (w *RotateFileWriter) openFile() error {
if w.file != nil {
logFallbackLogger.Warnf("[rotate_file_writer.removeOldFiles] cannot reopen log file \"%s\"", w.file.Name())
return nil
}
file, err := os.OpenFile(w.filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return errs.NewLoggingError(fmt.Sprintf("cannot open log file \"%s\", because %s", w.filePath, err.Error()), err)
}
w.file = file
return nil
}
func (w *RotateFileWriter) removeOldFiles() {
dir := filepath.Dir(w.filePath)
logBaseFileName := filepath.Base(w.filePath) + "."
allLogFiles, err := os.ReadDir(dir)
if err != nil {
return
}
retainMinUnixTime := int64(0)
if w.MaxFileDays > 0 {
retainMinUnixTime = time.Now().AddDate(0, 0, -int(w.MaxFileDays)).Unix()
}
for _, file := range allLogFiles {
if file.IsDir() {
continue
}
logFileName := filepath.Base(file.Name())
if !strings.HasPrefix(logFileName, logBaseFileName) {
continue
}
rotateDate := logFileName[len(logBaseFileName):]
dotIndex := strings.Index(rotateDate, ".")
if dotIndex > 0 {
rotateDate = rotateDate[0:dotIndex]
}
if len(rotateDate) != len(logRotateSuffixDateFormat) {
logFallbackLogger.Errorf("[rotate_file_writer.removeOldFiles] date suffix of old log file \"%s\" is invalid", file.Name())
continue
}
rotateDateTime, err := time.ParseInLocation(logRotateSuffixDateFormat, rotateDate, time.Now().Location())
if err != nil {
logFallbackLogger.Errorf("[rotate_file_writer.removeOldFiles] cannot parse rotate date of old log file \"%s\", because %s", file.Name(), err.Error())
continue
}
if rotateDateTime.Unix() >= retainMinUnixTime {
continue
}
err = os.Remove(filepath.Join(dir, file.Name()))
if err != nil {
logFallbackLogger.Errorf("[rotate_file_writer.removeOldFiles] cannot remove old log file \"%s\", because %s", file.Name(), err.Error())
}
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ func JWTTwoFactorAuthorization(c *core.Context) {
}
if claims.Type != core.USER_TOKEN_TYPE_REQUIRE_2FA {
log.WarnfWithRequestId(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%d\" token is not need two-factor authorization", claims.Uid)
log.WarnfWithRequestId(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%d\" token is not need two factor authorization", claims.Uid)
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenNotRequire2FA)
return
}
+1 -1
View File
@@ -16,7 +16,7 @@ func RequestId(config *settings.Config) core.MiddlewareHandlerFunc {
return
}
requestId := requestid.Container.Current.GenerateRequestId(c.ClientIP(), c.ClientPort())
requestId := requestid.Container.Current.GenerateRequestId(c.ClientIP())
c.SetRequestId(requestId)
if config.EnableRequestIdHeader {
+1 -24
View File
@@ -29,37 +29,14 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc {
config.MapProvider == settings.OpenTopoMapProvider ||
config.MapProvider == settings.OPNVKarteMapProvider ||
config.MapProvider == settings.CyclOSMMapProvider ||
config.MapProvider == settings.CartoDBMapProvider ||
config.MapProvider == settings.TomTomMapProvider ||
config.MapProvider == settings.TianDiTuProvider ||
config.MapProvider == settings.CustomProvider) {
config.MapProvider == settings.TomTomMapProvider) {
settingsArr = append(settingsArr, buildBooleanSetting("mp", config.EnableMapDataFetchProxy))
}
if config.MapProvider == settings.CustomProvider {
settingsArr = append(settingsArr, buildStringSetting("cmzl", fmt.Sprintf("%d-%d-%d", config.CustomMapTileServerMinZoomLevel, config.CustomMapTileServerMaxZoomLevel, config.CustomMapTileServerDefaultZoomLevel)))
if !config.EnableMapDataFetchProxy {
settingsArr = append(settingsArr, buildEncodedStringSetting("cmsu", config.CustomMapTileServerTileLayerUrl))
if config.CustomMapTileServerAnnotationLayerUrl != "" {
settingsArr = append(settingsArr, buildEncodedStringSetting("cmau", config.CustomMapTileServerAnnotationLayerUrl))
}
} else {
if config.CustomMapTileServerAnnotationLayerUrl != "" {
settingsArr = append(settingsArr, buildBooleanSetting("cmap", config.EnableMapDataFetchProxy))
}
}
}
if config.MapProvider == settings.TomTomMapProvider && config.TomTomMapAPIKey != "" && !config.EnableMapDataFetchProxy {
settingsArr = append(settingsArr, buildEncodedStringSetting("tmak", config.TomTomMapAPIKey))
}
if config.MapProvider == settings.TianDiTuProvider && config.TianDiTuAPIKey != "" && !config.EnableMapDataFetchProxy {
settingsArr = append(settingsArr, buildEncodedStringSetting("tdak", config.TianDiTuAPIKey))
}
if config.MapProvider == settings.GoogleMapProvider && config.GoogleMapAPIKey != "" {
settingsArr = append(settingsArr, buildEncodedStringSetting("gmak", config.GoogleMapAPIKey))
}
+9 -10
View File
@@ -69,16 +69,15 @@ type Account struct {
// AccountCreateRequest represents all parameters of account creation request
type AccountCreateRequest struct {
Name string `json:"name" binding:"required,notBlank,max=32"`
Category AccountCategory `json:"category" binding:"required"`
Type AccountType `json:"type" binding:"required"`
Icon int64 `json:"icon,string" binding:"required,min=1"`
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
Currency string `json:"currency" binding:"required,len=3,validCurrency"`
Balance int64 `json:"balance"`
Comment string `json:"comment" binding:"max=255"`
SubAccounts []*AccountCreateRequest `json:"subAccounts" binding:"omitempty"`
ClientSessionId string `json:"clientSessionId"`
Name string `json:"name" binding:"required,notBlank,max=32"`
Category AccountCategory `json:"category" binding:"required"`
Type AccountType `json:"type" binding:"required"`
Icon int64 `json:"icon,string" binding:"required,min=1"`
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
Currency string `json:"currency" binding:"required,len=3,validCurrency"`
Balance int64 `json:"balance"`
Comment string `json:"comment" binding:"max=255"`
SubAccounts []*AccountCreateRequest `json:"subAccounts" binding:"omitempty"`
}
// AccountModifyRequest represents all parameters of account modification request
+3 -4
View File
@@ -2,10 +2,9 @@ package models
// AuthResponse returns a view-object of user authorization
type AuthResponse struct {
Token string `json:"token"`
Need2FA bool `json:"need2FA"`
User *UserBasicInfo `json:"user"`
NotificationContent string `json:"notificationContent,omitempty"`
Token string `json:"token"`
Need2FA bool `json:"need2FA"`
User *UserBasicInfo `json:"user"`
}
// RegisterResponse returns a view-object of user register response
-1
View File
@@ -1 +0,0 @@
package models
-57
View File
@@ -1,57 +0,0 @@
package models
import "fmt"
// CurrencyDisplayType represents the display type of amount with currency
type CurrencyDisplayType byte
// Currency Display Type
const (
CURRENCY_DISPLAY_TYPE_DEFAULT CurrencyDisplayType = 0
CURRENCY_DISPLAY_TYPE_NONE CurrencyDisplayType = 1
CURRENCY_DISPLAY_TYPE_SYMBOL_BEFORE_AMOUNT CurrencyDisplayType = 2
CURRENCY_DISPLAY_TYPE_SYMBOL_AFTER_AMOUNT CurrencyDisplayType = 3
CURRENCY_DISPLAY_TYPE_SYMBOL_BEFORE_AMOUNT_WITHOUT_SPACE CurrencyDisplayType = 4
CURRENCY_DISPLAY_TYPE_SYMBOL_AFTER_AMOUNT_WITHOUT_SPACE CurrencyDisplayType = 5
CURRENCY_DISPLAY_TYPE_CODE_BEFORE_AMOUNT CurrencyDisplayType = 6
CURRENCY_DISPLAY_TYPE_CODE_AFTER_AMOUNT CurrencyDisplayType = 7
CURRENCY_DISPLAY_TYPE_UNIT_BEFORE_AMOUNT CurrencyDisplayType = 8
CURRENCY_DISPLAY_TYPE_UNIT_AFTER_AMOUNT CurrencyDisplayType = 9
CURRENCY_DISPLAY_TYPE_NAME_BEFORE_AMOUNT CurrencyDisplayType = 10
CURRENCY_DISPLAY_TYPE_NAME_AFTER_AMOUNT CurrencyDisplayType = 11
CURRENCY_DISPLAY_TYPE_INVALID CurrencyDisplayType = 255
)
// String returns a textual representation of the currency display type enum
func (d CurrencyDisplayType) String() string {
switch d {
case CURRENCY_DISPLAY_TYPE_DEFAULT:
return "Default"
case CURRENCY_DISPLAY_TYPE_NONE:
return "None"
case CURRENCY_DISPLAY_TYPE_SYMBOL_BEFORE_AMOUNT:
return "Symbol Before Amount"
case CURRENCY_DISPLAY_TYPE_SYMBOL_AFTER_AMOUNT:
return "Symbol After Amount"
case CURRENCY_DISPLAY_TYPE_SYMBOL_BEFORE_AMOUNT_WITHOUT_SPACE:
return "Symbol Before Amount Without Space"
case CURRENCY_DISPLAY_TYPE_SYMBOL_AFTER_AMOUNT_WITHOUT_SPACE:
return "Symbol After Amount Without Space"
case CURRENCY_DISPLAY_TYPE_CODE_BEFORE_AMOUNT:
return "Code Before Amount"
case CURRENCY_DISPLAY_TYPE_CODE_AFTER_AMOUNT:
return "Code After Amount"
case CURRENCY_DISPLAY_TYPE_UNIT_BEFORE_AMOUNT:
return "Unit Before Amount"
case CURRENCY_DISPLAY_TYPE_UNIT_AFTER_AMOUNT:
return "Unit After Amount"
case CURRENCY_DISPLAY_TYPE_NAME_BEFORE_AMOUNT:
return "Name Before Amount"
case CURRENCY_DISPLAY_TYPE_NAME_AFTER_AMOUNT:
return "Name After Amount"
case CURRENCY_DISPLAY_TYPE_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(d))
}
}
-1
View File
@@ -11,5 +11,4 @@ type DataStatisticsResponse struct {
TotalTransactionCategoryCount int64 `json:"totalTransactionCategoryCount,string"`
TotalTransactionTagCount int64 `json:"totalTransactionTagCount,string"`
TotalTransactionCount int64 `json:"totalTransactionCount,string"`
TotalTransactionTemplateCount int64 `json:"totalTransactionTemplateCount,string"`
}
+13 -14
View File
@@ -7,14 +7,13 @@ const TokenMaxUserAgentLength = 255
// TokenRecord represents token data stored in database
type TokenRecord struct {
Uid int64 `xorm:"PK INDEX(IDX_token_record_uid_type_expired_time)"`
UserTokenId int64 `xorm:"PK"`
TokenType core.TokenType `xorm:"INDEX(IDX_token_record_uid_type_expired_time) TINYINT NOT NULL"`
Secret string `xorm:"VARCHAR(10) NOT NULL"`
UserAgent string `xorm:"VARCHAR(255)"`
CreatedUnixTime int64 `xorm:"PK"`
ExpiredUnixTime int64 `xorm:"INDEX(IDX_token_record_uid_type_expired_time)"`
LastSeenUnixTime int64
Uid int64 `xorm:"PK INDEX(IDX_token_record_uid_type_expired_time)"`
UserTokenId int64 `xorm:"PK"`
TokenType core.TokenType `xorm:"INDEX(IDX_token_record_uid_type_expired_time) TINYINT NOT NULL"`
Secret string `xorm:"VARCHAR(10) NOT NULL"`
UserAgent string `xorm:"VARCHAR(255)"`
CreatedUnixTime int64 `xorm:"PK"`
ExpiredUnixTime int64 `xorm:"INDEX(IDX_token_record_uid_type_expired_time)"`
}
// TokenRevokeRequest represents all parameters of token revoking request
@@ -24,10 +23,9 @@ type TokenRevokeRequest struct {
// TokenRefreshResponse represents all parameters of token refreshing request
type TokenRefreshResponse struct {
NewToken string `json:"newToken,omitempty"`
OldTokenId string `json:"oldTokenId,omitempty"`
User *UserBasicInfo `json:"user"`
NotificationContent string `json:"notificationContent,omitempty"`
NewToken string `json:"newToken"`
OldTokenId string `json:"oldTokenId"`
User *UserBasicInfo `json:"user"`
}
// TokenInfoResponse represents a view-object of token
@@ -35,7 +33,8 @@ type TokenInfoResponse struct {
TokenId string `json:"tokenId"`
TokenType core.TokenType `json:"tokenType"`
UserAgent string `json:"userAgent"`
LastSeen int64 `json:"lastSeen"`
CreatedAt int64 `json:"createdAt"`
ExpiredAt int64 `json:"expiredAt"`
IsCurrent bool `json:"isCurrent"`
}
@@ -54,5 +53,5 @@ func (a TokenInfoResponseSlice) Swap(i, j int) {
// Less reports whether the first item is less than the second one
func (a TokenInfoResponseSlice) Less(i, j int) bool {
return a[i].LastSeen > a[j].LastSeen
return a[i].ExpiredAt > a[j].ExpiredAt
}
+51 -56
View File
@@ -1,7 +1,9 @@
package models
import (
"fmt"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/utils"
@@ -74,7 +76,6 @@ type TransactionCreateRequest struct {
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"`
ClientSessionId string `json:"clientSessionId"`
}
// TransactionModifyRequest represents all parameters of transaction modification request
@@ -95,23 +96,19 @@ type TransactionModifyRequest struct {
// TransactionCountRequest represents transaction count request
type TransactionCountRequest struct {
Type TransactionDbType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagIds string `form:"tag_ids"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"`
MinTime int64 `form:"min_time" binding:"min=0"`
Type TransactionDbType `form:"type" binding:"min=0,max=4"`
CategoryId int64 `form:"category_id" binding:"min=0"`
AccountId int64 `form:"account_id" binding:"min=0"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"`
MinTime int64 `form:"min_time" binding:"min=0"`
}
// TransactionListByMaxTimeRequest represents all parameters of transaction listing by max time request
type TransactionListByMaxTimeRequest struct {
Type TransactionDbType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagIds string `form:"tag_ids"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
CategoryId int64 `form:"category_id" binding:"min=0"`
AccountId int64 `form:"account_id" binding:"min=0"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"`
MinTime int64 `form:"min_time" binding:"min=0"`
@@ -128,10 +125,8 @@ type TransactionListInMonthByPageRequest struct {
Year int32 `form:"year" binding:"required,min=1"`
Month int32 `form:"month" binding:"required,min=1"`
Type TransactionDbType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagIds string `form:"tag_ids"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
CategoryId int64 `form:"category_id" binding:"min=0"`
AccountId int64 `form:"account_id" binding:"min=0"`
Keyword string `form:"keyword"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
@@ -140,21 +135,13 @@ type TransactionListInMonthByPageRequest struct {
// TransactionStatisticRequest represents all parameters of transaction statistic request
type TransactionStatisticRequest struct {
StartTime int64 `form:"start_time" binding:"min=0"`
EndTime int64 `form:"end_time" binding:"min=0"`
UseTransactionTimezone bool `form:"use_transaction_timezone"`
}
// TransactionStatisticTrendsRequest represents all parameters of transaction statistic trends request
type TransactionStatisticTrendsRequest struct {
YearMonthRangeRequest
UseTransactionTimezone bool `form:"use_transaction_timezone"`
StartTime int64 `form:"start_time" binding:"min=0"`
EndTime int64 `form:"end_time" binding:"min=0"`
}
// TransactionAmountsRequest represents all parameters of transaction amounts request
type TransactionAmountsRequest struct {
Query string `form:"query"`
UseTransactionTimezone bool `form:"use_transaction_timezone"`
Query string `form:"query"`
}
// TransactionAmountsRequestItem represents an item of transaction amounts request
@@ -164,6 +151,12 @@ type TransactionAmountsRequestItem struct {
EndTime int64
}
// TransactionMonthAmountsRequest represents all parameters of transaction month amounts request
type TransactionMonthAmountsRequest struct {
StartYearMonth string `form:"start_year_month"`
EndYearMonth string `form:"end_year_month"`
}
// TransactionGetRequest represents all parameters of transaction getting request
type TransactionGetRequest struct {
Id int64 `form:"id,string" binding:"required,min=1"`
@@ -177,10 +170,14 @@ type TransactionDeleteRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
}
// YearMonthRangeRequest represents all parameters of a request with year and month range
type YearMonthRangeRequest struct {
StartYearMonth string `form:"start_year_month"`
EndYearMonth string `form:"end_year_month"`
// TransactionAccountsAmount represents transaction accounts amount map
type TransactionAccountsAmount map[int64]*TransactionAccountAmount
// TransactionAccountAmount represents transaction account amount
type TransactionAccountAmount struct {
AccountId int64
TotalIncomeAmount int64
TotalExpenseAmount int64
}
// TransactionGeoLocationResponse represents a view-object of transaction geographic location info
@@ -230,7 +227,7 @@ type TransactionInfoPageWrapperResponse2 struct {
TotalCount int64 `json:"totalCount"`
}
// TransactionStatisticResponse represents transaction statistic response
// TransactionStatisticResponse represents an item of transaction amounts
type TransactionStatisticResponse struct {
StartTime int64 `json:"startTime"`
EndTime int64 `json:"endTime"`
@@ -244,13 +241,6 @@ type TransactionStatisticResponseItem struct {
TotalAmount int64 `json:"amount"`
}
// TransactionStatisticTrendsItem represents the data within each statistic interval
type TransactionStatisticTrendsItem struct {
Year int32 `json:"year"`
Month int32 `json:"month"`
Items []*TransactionStatisticResponseItem `json:"items"`
}
// TransactionAmountsResponseItem represents an item of transaction amounts
type TransactionAmountsResponseItem struct {
StartTime int64 `json:"startTime"`
@@ -390,28 +380,33 @@ func (t *TransactionAmountsRequest) GetTransactionAmountsRequestItems() ([]*Tran
return requestItems, nil
}
// GetNumericYearMonthRange returns numeric start year, start month, end year and end month
func (t *YearMonthRangeRequest) GetNumericYearMonthRange() (int32, int32, int32, int32, error) {
var startYear, startMonth, endYear, endMonth int32
var err error
// GetStartTimeAndEndTime returns start unix time and end unix time by request parameter
func (t *TransactionMonthAmountsRequest) GetStartTimeAndEndTime(utcOffset int16) (int64, int64, error) {
startUnixTime := int64(0)
endUnixTime := time.Now().Unix()
if t.StartYearMonth != "" {
startYear, startMonth, err = utils.ParseNumericYearMonth(t.StartYearMonth)
startTime, err := utils.ParseFromShortDateTime(fmt.Sprintf("%s-1 0:0:0", t.StartYearMonth), utcOffset)
if err != nil {
return 0, 0, 0, 0, err
return 0, 0, err
}
startUnixTime = startTime.Unix()
}
if t.EndYearMonth != "" {
endYear, endMonth, err = utils.ParseNumericYearMonth(t.EndYearMonth)
endTime, err := utils.ParseFromShortDateTime(fmt.Sprintf("%s-1 0:0:0", t.EndYearMonth), utcOffset)
if err != nil {
return 0, 0, 0, 0, err
return 0, 0, err
}
endTime = endTime.AddDate(0, 1, 0)
endUnixTime = endTime.Unix() - 1
}
return startYear, startMonth, endYear, endMonth, nil
return startUnixTime, endUnixTime, nil
}
// TransactionInfoResponseSlice represents the slice data structure of TransactionInfoResponse
@@ -436,26 +431,26 @@ func (s TransactionInfoResponseSlice) Less(i, j int) bool {
return s[i].Id > s[j].Id
}
// TransactionStatisticTrendsItemSlice represents the slice data structure of TransactionStatisticTrendsItem
type TransactionStatisticTrendsItemSlice []*TransactionStatisticTrendsItem
// TransactionMonthAmountsResponseItemSlice represents the slice data structure of TransactionMonthAmountsResponseItem
type TransactionMonthAmountsResponseItemSlice []*TransactionMonthAmountsResponseItem
// Len returns the count of items
func (s TransactionStatisticTrendsItemSlice) Len() int {
func (s TransactionMonthAmountsResponseItemSlice) Len() int {
return len(s)
}
// Swap swaps two items
func (s TransactionStatisticTrendsItemSlice) Swap(i, j int) {
func (s TransactionMonthAmountsResponseItemSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Less reports whether the first item is less than the second one
func (s TransactionStatisticTrendsItemSlice) Less(i, j int) bool {
func (s TransactionMonthAmountsResponseItemSlice) Less(i, j int) bool {
if s[i].Year != s[j].Year {
return s[i].Year < s[j].Year
return s[i].Year > s[j].Year
}
return s[i].Month < s[j].Month
return s[i].Month > s[j].Month
}
// TransactionAmountsResponseItemAmountInfoSlice represents the slice data structure of TransactionAmountsResponseItemAmountInfo
+12 -14
View File
@@ -44,13 +44,12 @@ type TransactionCategoryGetRequest struct {
// TransactionCategoryCreateRequest represents all parameters of single transaction category creation request
type TransactionCategoryCreateRequest struct {
Name string `json:"name" binding:"required,notBlank,max=32"`
Type TransactionCategoryType `json:"type" binding:"required"`
ParentId int64 `json:"parentId,string" binding:"min=0"`
Icon int64 `json:"icon,string" binding:"min=1"`
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
Comment string `json:"comment" binding:"max=255"`
ClientSessionId string `json:"clientSessionId"`
Name string `json:"name" binding:"required,notBlank,max=32"`
Type TransactionCategoryType `json:"type" binding:"required"`
ParentId int64 `json:"parentId,string" binding:"min=0"`
Icon int64 `json:"icon,string" binding:"min=1"`
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
Comment string `json:"comment" binding:"max=255"`
}
// TransactionCategoryCreateBatchRequest represents all parameters of transaction category batch creation request
@@ -70,13 +69,12 @@ type TransactionCategoryCreateWithSubCategories struct {
// TransactionCategoryModifyRequest represents all parameters of transaction category modification request
type TransactionCategoryModifyRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
Name string `json:"name" binding:"required,notBlank,max=32"`
ParentId int64 `json:"parentId,string" binding:"min=0"`
Icon int64 `json:"icon,string" binding:"min=1"`
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
Comment string `json:"comment" binding:"max=255"`
Hidden bool `json:"hidden"`
Id int64 `json:"id,string" binding:"required,min=1"`
Name string `json:"name" binding:"required,notBlank,max=32"`
Icon int64 `json:"icon,string" binding:"min=1"`
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
Comment string `json:"comment" binding:"max=255"`
Hidden bool `json:"hidden"`
}
// TransactionCategoryHideRequest represents all parameters of transaction category hiding request
+4 -4
View File
@@ -3,10 +3,10 @@ package models
// TransactionTag represents transaction tag data stored in database
type TransactionTag struct {
TagId int64 `xorm:"PK"`
Uid int64 `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
Deleted bool `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
Name string `xorm:"VARCHAR(32) NOT NULL"`
DisplayOrder int32 `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
Uid int64 `xorm:"INDEX(IDX_tag_uid_deleted_name) NOT NULL"`
Deleted bool `xorm:"INDEX(IDX_tag_uid_deleted_name) NOT NULL"`
Name string `xorm:"INDEX(IDX_tag_uid_deleted_name) VARCHAR(32) NOT NULL"`
DisplayOrder int32 `xorm:"NOT NULL"`
Hidden bool `xorm:"NOT NULL"`
CreatedUnixTime int64
UpdatedUnixTime int64
+4 -4
View File
@@ -3,11 +3,11 @@ package models
// TransactionTagIndex represents transaction and transaction tag relation stored in database
type TransactionTagIndex struct {
TagIndexId int64 `xorm:"PK"`
Uid int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_id) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_time_tag_id) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_id)"`
Deleted bool `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_id) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_time_tag_id) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_id) NOT NULL"`
TransactionTime int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_transaction_time_tag_id) NOT NULL"`
TagId int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_id) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_time_tag_id)"`
Uid int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_id) INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_time) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_id)"`
Deleted bool `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_id) INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_time) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_id) NOT NULL"`
TagId int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_id) INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_time)"`
TransactionId int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_id) INDEX(IDX_transaction_tag_index_uid_deleted_transaction_id)"`
TransactionTime int64 `xorm:"INDEX(IDX_transaction_tag_index_uid_deleted_tag_id_transaction_time) NOT NULL"`
CreatedUnixTime int64
UpdatedUnixTime int64
DeletedUnixTime int64
-171
View File
@@ -1,171 +0,0 @@
package models
import (
"strings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// TransactionTemplateType represents transaction template type in database
type TransactionTemplateType byte
// Transaction template types
const (
TRANSACTION_TEMPLATE_TYPE_NORMAL TransactionTemplateType = 1
)
// TransactionTemplate represents transaction template stored in database
type TransactionTemplate struct {
TemplateId int64 `xorm:"PK"`
Uid int64 `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
Deleted bool `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
TemplateType TransactionTemplateType `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
Name string `xorm:"VARCHAR(32) NOT NULL"`
Type TransactionType `xorm:"NOT NULL"`
CategoryId int64 `xorm:"NOT NULL"`
AccountId int64 `xorm:"NOT NULL"`
TagIds string `xorm:"VARCHAR(255) NOT NULL"`
Amount int64 `xorm:"NOT NULL"`
RelatedAccountId int64 `xorm:"NOT NULL"`
RelatedAccountAmount int64 `xorm:"NOT NULL"`
HideAmount bool `xorm:"NOT NULL"`
Comment string `xorm:"VARCHAR(255) NOT NULL"`
DisplayOrder int32 `xorm:"INDEX(IDX_transaction_uid_deleted_template_type_order) NOT NULL"`
Hidden bool `xorm:"NOT NULL"`
CreatedUnixTime int64
UpdatedUnixTime int64
DeletedUnixTime int64
}
// TransactionTemplateListRequest represents all parameters of transaction template list request
type TransactionTemplateListRequest struct {
TemplateType TransactionTemplateType `form:"templateType"`
}
// TransactionTemplateGetRequest represents all parameters of transaction template getting request
type TransactionTemplateGetRequest struct {
Id int64 `form:"id,string" binding:"required,min=1"`
}
// TransactionTemplateCreateRequest represents all parameters of transaction template creation request
type TransactionTemplateCreateRequest struct {
TemplateType TransactionTemplateType `json:"templateType"`
Name string `json:"name" binding:"required,notBlank,max=32"`
Type TransactionType `json:"type" binding:"required"`
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
ClientSessionId string `json:"clientSessionId"`
}
// TransactionTemplateModifyNameRequest represents all parameters of transaction template name modification request
type TransactionTemplateModifyNameRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
Name string `json:"name" binding:"required,notBlank,max=32"`
}
// TransactionTemplateModifyRequest represents all parameters of transaction template modification request
type TransactionTemplateModifyRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
Name string `json:"name" binding:"required,notBlank,max=32"`
Type TransactionType `json:"type" binding:"required"`
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
DestinationAccountId int64 `json:"destinationAccountId,string" binding:"min=0"`
SourceAmount int64 `json:"sourceAmount" binding:"min=-99999999999,max=99999999999"`
DestinationAmount int64 `json:"destinationAmount" binding:"min=-99999999999,max=99999999999"`
HideAmount bool `json:"hideAmount"`
TagIds []string `json:"tagIds"`
Comment string `json:"comment" binding:"max=255"`
}
// TransactionTemplateHideRequest represents all parameters of transaction template hiding request
type TransactionTemplateHideRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
Hidden bool `json:"hidden"`
}
// TransactionTemplateMoveRequest represents all parameters of transaction template moving request
type TransactionTemplateMoveRequest struct {
NewDisplayOrders []*TransactionTemplateNewDisplayOrderRequest `json:"newDisplayOrders"`
}
// TransactionTemplateNewDisplayOrderRequest represents a data pair of id and display order
type TransactionTemplateNewDisplayOrderRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
DisplayOrder int32 `json:"displayOrder"`
}
// TransactionTemplateDeleteRequest represents all parameters of transaction template deleting request
type TransactionTemplateDeleteRequest struct {
Id int64 `json:"id,string" binding:"required,min=1"`
}
type TransactionTemplateInfoResponse struct {
*TransactionInfoResponse
TemplateType TransactionTemplateType `json:"templateType"`
Name string `json:"name"`
DisplayOrder int32 `json:"displayOrder"`
Hidden bool `json:"hidden"`
}
// ToTransactionInfoResponse returns a view-object according to database model
func (t *TransactionTemplate) ToTransactionInfoResponse(utcOffset int16) *TransactionInfoResponse {
tagIds := make([]string, 0, 0)
if t.TagIds != "" {
tagIds = strings.Split(t.TagIds, ",")
}
return &TransactionInfoResponse{
Id: t.TemplateId,
TimeSequenceId: utils.GetMinTransactionTimeFromUnixTime(t.CreatedUnixTime),
Type: t.Type,
CategoryId: t.CategoryId,
Time: 0,
UtcOffset: utcOffset,
SourceAccountId: t.AccountId,
DestinationAccountId: t.RelatedAccountId,
SourceAmount: t.Amount,
DestinationAmount: t.RelatedAccountAmount,
HideAmount: t.HideAmount,
TagIds: tagIds,
Comment: t.Comment,
GeoLocation: nil,
Editable: true,
}
}
// ToTransactionTemplateInfoResponse returns a view-object according to database model
func (t *TransactionTemplate) ToTransactionTemplateInfoResponse(utcOffset int16) *TransactionTemplateInfoResponse {
return &TransactionTemplateInfoResponse{
TransactionInfoResponse: t.ToTransactionInfoResponse(utcOffset),
TemplateType: t.TemplateType,
Name: t.Name,
DisplayOrder: t.DisplayOrder,
Hidden: t.Hidden,
}
}
// TransactionTemplateInfoResponseSlice represents the slice data structure of TransactionTemplateInfoResponse
type TransactionTemplateInfoResponseSlice []*TransactionTemplateInfoResponse
// Len returns the count of items
func (s TransactionTemplateInfoResponseSlice) Len() int {
return len(s)
}
// Swap swaps two items
func (s TransactionTemplateInfoResponseSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Less reports whether the first item is less than the second one
func (s TransactionTemplateInfoResponseSlice) Less(i, j int) bool {
return s[i].DisplayOrder < s[j].DisplayOrder
}
+73 -107
View File
@@ -4,7 +4,6 @@ import (
"fmt"
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
@@ -12,7 +11,7 @@ import (
// TransactionEditScope represents the scope which transaction can be edited
type TransactionEditScope byte
// Editable Transaction Ranges
// Editable Transaction Scopes
const (
TRANSACTION_EDIT_SCOPE_NONE TransactionEditScope = 0
TRANSACTION_EDIT_SCOPE_ALL TransactionEditScope = 1
@@ -24,7 +23,7 @@ const (
TRANSACTION_EDIT_SCOPE_INVALID TransactionEditScope = 255
)
// String returns a textual representation of the editable transaction ranges enum
// String returns a textual representation of the editable transaction scopes enum
func (s TransactionEditScope) String() string {
switch s {
case TRANSACTION_EDIT_SCOPE_NONE:
@@ -48,39 +47,6 @@ func (s TransactionEditScope) String() string {
}
}
// AmountColorType represents the type of amount color in frontend
type AmountColorType byte
// Amount Color Types
const (
AMOUNT_COLOR_TYPE_DEFAULT AmountColorType = 0
AMOUNT_COLOR_TYPE_GREEN AmountColorType = 1
AMOUNT_COLOR_TYPE_RED AmountColorType = 2
AMOUNT_COLOR_TYPE_YELLOW AmountColorType = 3
AMOUNT_COLOR_TYPE_BLACK_OR_WHITE AmountColorType = 4
AMOUNT_COLOR_TYPE_INVALID AmountColorType = 255
)
// String returns a textual representation of the amount color type enum
func (s AmountColorType) String() string {
switch s {
case AMOUNT_COLOR_TYPE_DEFAULT:
return "Default"
case AMOUNT_COLOR_TYPE_GREEN:
return "Green"
case AMOUNT_COLOR_TYPE_RED:
return "Red"
case AMOUNT_COLOR_TYPE_YELLOW:
return "Yellow"
case AMOUNT_COLOR_TYPE_BLACK_OR_WHITE:
return "Black or White"
case AMOUNT_COLOR_TYPE_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(s))
}
}
// User represents user data stored in database
type User struct {
Uid int64 `xorm:"PK"`
@@ -89,22 +55,15 @@ type User struct {
Nickname string `xorm:"VARCHAR(64) NOT NULL"`
Password string `xorm:"VARCHAR(64) NOT NULL"`
Salt string `xorm:"VARCHAR(10) NOT NULL"`
CustomAvatarType string `xorm:"VARCHAR(10)"`
DefaultAccountId int64
TransactionEditScope TransactionEditScope `xorm:"TINYINT NOT NULL"`
Language string `xorm:"VARCHAR(10)"`
DefaultCurrency string `xorm:"VARCHAR(3) NOT NULL"`
FirstDayOfWeek WeekDay `xorm:"TINYINT NOT NULL"`
LongDateFormat LongDateFormat `xorm:"TINYINT"`
ShortDateFormat ShortDateFormat `xorm:"TINYINT"`
LongTimeFormat LongTimeFormat `xorm:"TINYINT"`
ShortTimeFormat ShortTimeFormat `xorm:"TINYINT"`
DecimalSeparator core.DecimalSeparator `xorm:"TINYINT"`
DigitGroupingSymbol core.DigitGroupingSymbol `xorm:"TINYINT"`
DigitGrouping core.DigitGroupingType `xorm:"TINYINT"`
CurrencyDisplayType CurrencyDisplayType `xorm:"TINYINT"`
ExpenseAmountColor AmountColorType `xorm:"TINYINT"`
IncomeAmountColor AmountColorType `xorm:"TINYINT"`
TransactionEditScope TransactionEditScope `xorm:"TINYINT NOT NULL"`
Language string `xorm:"VARCHAR(10)"`
DefaultCurrency string `xorm:"VARCHAR(3) NOT NULL"`
FirstDayOfWeek WeekDay `xorm:"TINYINT NOT NULL"`
LongDateFormat LongDateFormat `xorm:"TINYINT"`
ShortDateFormat ShortDateFormat `xorm:"TINYINT"`
LongTimeFormat LongTimeFormat `xorm:"TINYINT"`
ShortTimeFormat ShortTimeFormat `xorm:"TINYINT"`
Disabled bool
Deleted bool `xorm:"NOT NULL"`
EmailVerified bool `xorm:"NOT NULL"`
@@ -116,27 +75,21 @@ type User struct {
// UserBasicInfo represents a view-object of user basic info
type UserBasicInfo struct {
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar"`
AvatarProvider string `json:"avatarProvider,omitempty"`
DefaultAccountId int64 `json:"defaultAccountId,string"`
TransactionEditScope TransactionEditScope `json:"transactionEditScope"`
Language string `json:"language"`
DefaultCurrency string `json:"defaultCurrency"`
FirstDayOfWeek WeekDay `json:"firstDayOfWeek"`
LongDateFormat LongDateFormat `json:"longDateFormat"`
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
DecimalSeparator core.DecimalSeparator `json:"decimalSeparator"`
DigitGroupingSymbol core.DigitGroupingSymbol `json:"digitGroupingSymbol"`
DigitGrouping core.DigitGroupingType `json:"digitGrouping"`
CurrencyDisplayType CurrencyDisplayType `json:"currencyDisplayType"`
ExpenseAmountColor AmountColorType `json:"expenseAmountColor"`
IncomeAmountColor AmountColorType `json:"incomeAmountColor"`
EmailVerified bool `json:"emailVerified"`
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar"`
AvatarProvider string `json:"avatarProvider,omitempty"`
DefaultAccountId int64 `json:"defaultAccountId,string"`
TransactionEditScope TransactionEditScope `json:"transactionEditScope"`
Language string `json:"language"`
DefaultCurrency string `json:"defaultCurrency"`
FirstDayOfWeek WeekDay `json:"firstDayOfWeek"`
LongDateFormat LongDateFormat `json:"longDateFormat"`
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
EmailVerified bool `json:"emailVerified"`
}
// UserLoginRequest represents all parameters of user login request
@@ -164,9 +117,8 @@ type UserVerifyEmailRequest struct {
// UserVerifyEmailResponse represents all response parameters after user have verified email
type UserVerifyEmailResponse struct {
NewToken string `json:"newToken,omitempty"`
User *UserBasicInfo `json:"user"`
NotificationContent string `json:"notificationContent,omitempty"`
NewToken string `json:"newToken,omitempty"`
User *UserBasicInfo `json:"user"`
}
// UserResendVerifyEmailRequest represents all parameters of user resend verify email request
@@ -177,25 +129,19 @@ type UserResendVerifyEmailRequest struct {
// UserProfileUpdateRequest represents all parameters of user updating profile request
type UserProfileUpdateRequest struct {
Email string `json:"email" binding:"omitempty,notBlank,max=100,validEmail"`
Nickname string `json:"nickname" binding:"omitempty,notBlank,max=64"`
Password string `json:"password" binding:"omitempty,min=6,max=128"`
OldPassword string `json:"oldPassword" binding:"omitempty,min=6,max=128"`
DefaultAccountId int64 `json:"defaultAccountId,string" binding:"omitempty,min=1"`
TransactionEditScope *TransactionEditScope `json:"transactionEditScope" binding:"omitempty,min=0,max=6"`
Language string `json:"language" binding:"omitempty,min=2,max=16"`
DefaultCurrency string `json:"defaultCurrency" binding:"omitempty,len=3,validCurrency"`
FirstDayOfWeek *WeekDay `json:"firstDayOfWeek" binding:"omitempty,min=0,max=6"`
LongDateFormat *LongDateFormat `json:"longDateFormat" binding:"omitempty,min=0,max=3"`
ShortDateFormat *ShortDateFormat `json:"shortDateFormat" binding:"omitempty,min=0,max=3"`
LongTimeFormat *LongTimeFormat `json:"longTimeFormat" binding:"omitempty,min=0,max=3"`
ShortTimeFormat *ShortTimeFormat `json:"shortTimeFormat" binding:"omitempty,min=0,max=3"`
DecimalSeparator *core.DecimalSeparator `json:"decimalSeparator" binding:"omitempty,min=0,max=3"`
DigitGroupingSymbol *core.DigitGroupingSymbol `json:"digitGroupingSymbol" binding:"omitempty,min=0,max=4"`
DigitGrouping *core.DigitGroupingType `json:"digitGrouping" binding:"omitempty,min=0,max=2"`
CurrencyDisplayType *CurrencyDisplayType `json:"currencyDisplayType" binding:"omitempty,min=0,max=11"`
ExpenseAmountColor *AmountColorType `json:"expenseAmountColor" binding:"omitempty,min=0,max=4"`
IncomeAmountColor *AmountColorType `json:"incomeAmountColor" binding:"omitempty,min=0,max=4"`
Email string `json:"email" binding:"omitempty,notBlank,max=100,validEmail"`
Nickname string `json:"nickname" binding:"omitempty,notBlank,max=64"`
Password string `json:"password" binding:"omitempty,min=6,max=128"`
OldPassword string `json:"oldPassword" binding:"omitempty,min=6,max=128"`
DefaultAccountId int64 `json:"defaultAccountId,string" binding:"omitempty,min=1"`
TransactionEditScope *TransactionEditScope `json:"transactionEditScope" binding:"omitempty,min=0,max=7"`
Language string `json:"language" binding:"omitempty,min=2,max=16"`
DefaultCurrency string `json:"defaultCurrency" binding:"omitempty,len=3,validCurrency"`
FirstDayOfWeek *WeekDay `json:"firstDayOfWeek" binding:"omitempty,min=0,max=6"`
LongDateFormat *LongDateFormat `json:"longDateFormat" binding:"omitempty,min=0,max=3"`
ShortDateFormat *ShortDateFormat `json:"shortDateFormat" binding:"omitempty,min=0,max=3"`
LongTimeFormat *LongTimeFormat `json:"longTimeFormat" binding:"omitempty,min=0,max=3"`
ShortTimeFormat *ShortTimeFormat `json:"shortTimeFormat" binding:"omitempty,min=0,max=3"`
}
// UserProfileUpdateResponse represents the data returns to frontend after updating profile
@@ -206,8 +152,22 @@ type UserProfileUpdateResponse struct {
// UserProfileResponse represents a view-object of user profile
type UserProfileResponse struct {
*UserBasicInfo
LastLoginAt int64 `json:"lastLoginAt"`
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
AvatarUrl string `json:"avatar"`
AvatarProvider string `json:"avatarProvider,omitempty"`
DefaultAccountId int64 `json:"defaultAccountId,string"`
TransactionEditScope TransactionEditScope `json:"transactionEditScope"`
Language string `json:"language"`
DefaultCurrency string `json:"defaultCurrency"`
FirstDayOfWeek WeekDay `json:"firstDayOfWeek"`
LongDateFormat LongDateFormat `json:"longDateFormat"`
ShortDateFormat ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat ShortTimeFormat `json:"shortTimeFormat"`
EmailVerified bool `json:"emailVerified"`
LastLoginAt int64 `json:"lastLoginAt"`
}
// CanEditTransactionByTransactionTime returns whether this user can edit transaction with specified transaction time
@@ -269,12 +229,6 @@ func (u *User) ToUserBasicInfo() *UserBasicInfo {
ShortDateFormat: u.ShortDateFormat,
LongTimeFormat: u.LongTimeFormat,
ShortTimeFormat: u.ShortTimeFormat,
DecimalSeparator: u.DecimalSeparator,
DigitGroupingSymbol: u.DigitGroupingSymbol,
DigitGrouping: u.DigitGrouping,
CurrencyDisplayType: u.CurrencyDisplayType,
ExpenseAmountColor: u.ExpenseAmountColor,
IncomeAmountColor: u.IncomeAmountColor,
EmailVerified: u.EmailVerified,
}
}
@@ -282,8 +236,22 @@ func (u *User) ToUserBasicInfo() *UserBasicInfo {
// ToUserProfileResponse returns a user profile view-object according to database model
func (u *User) ToUserProfileResponse() *UserProfileResponse {
return &UserProfileResponse{
UserBasicInfo: u.ToUserBasicInfo(),
LastLoginAt: u.LastLoginUnixTime,
Username: u.Username,
Email: u.Email,
Nickname: u.Nickname,
AvatarUrl: u.getAvatarUrl(),
AvatarProvider: u.getAvatarProvider(),
DefaultAccountId: u.DefaultAccountId,
TransactionEditScope: u.TransactionEditScope,
Language: u.Language,
DefaultCurrency: u.DefaultCurrency,
FirstDayOfWeek: u.FirstDayOfWeek,
LongDateFormat: u.LongDateFormat,
ShortDateFormat: u.ShortDateFormat,
LongTimeFormat: u.LongTimeFormat,
ShortTimeFormat: u.ShortTimeFormat,
EmailVerified: u.EmailVerified,
LastLoginAt: u.LastLoginUnixTime,
}
}
@@ -294,9 +262,7 @@ func (u *User) getAvatarProvider() string {
func (u *User) getAvatarUrl() string {
avatarProvider := settings.Container.Current.AvatarProvider
if avatarProvider == settings.InternalAvatarProvider {
return utils.GetInternalAvatarUrl(u.Uid, u.CustomAvatarType, settings.Container.Current.RootUrl)
} else if avatarProvider == settings.GravatarProvider {
if avatarProvider == settings.GravatarProvider {
return utils.GetGravatarUrl(u.Email)
}
+31 -35
View File
@@ -6,6 +6,7 @@ import (
"encoding/hex"
"fmt"
"hash/crc32"
"math"
"net"
"sync/atomic"
"time"
@@ -18,18 +19,15 @@ import (
// Length and mask of all information in request id
const (
requestIdLength = 36
secondsTodayBits = 17
secondsTodayBitsMask = (1 << secondsTodayBits) - 1
clientPortNumberAllBits = 16
clientPortNumberHigh1Bit = 1
clientPortNumberLow15Bits = clientPortNumberAllBits - clientPortNumberHigh1Bit
clientPortNumberHigh1BitMask = 1 << clientPortNumberLow15Bits
clientPortNumberLow15BitsMask = clientPortNumberHigh1BitMask - 1
reqSeqNumberBits = 30
reqSeqNumberBitsMask = (1 << reqSeqNumberBits) - 1
clientIpv6Bit = 1
clientIpv6BitMask = 1
requestIdLength = 36
secondsTodayBits = 17
secondsTodayBitsMask = (1 << secondsTodayBits) - 1
randomNumberBits = 15
randomNumberBitsMask = (1 << randomNumberBits) - 1
reqSeqNumberBits = 31
reqSeqNumberBitsMask = (1 << reqSeqNumberBits) - 1
clientIpv6Bit = 1
clientIpv6BitMask = 1
)
// RequestIdInfo represents a struct which has all information in request id
@@ -37,10 +35,10 @@ type RequestIdInfo struct {
ServerUniqId uint16
InstanceUniqId uint16
SecondsElapsedToday uint32
RandomNumber uint32
RequestSeqId uint32
IsClientIpv6 bool
ClientIp uint32
ClientPort uint16
}
// DefaultRequestIdGenerator represents default request id generator
@@ -123,7 +121,7 @@ func (r *DefaultRequestIdGenerator) GetCurrentInstanceUniqId() uint16 {
}
// GenerateRequestId returns a new request id
func (r *DefaultRequestIdGenerator) GenerateRequestId(clientIpAddr string, clientPort uint16) string {
func (r *DefaultRequestIdGenerator) GenerateRequestId(clientIpAddr string) string {
ip := net.ParseIP(clientIpAddr)
isClientIpv6 := ip.To4() == nil
var clientIp uint32
@@ -134,38 +132,38 @@ func (r *DefaultRequestIdGenerator) GenerateRequestId(clientIpAddr string, clien
clientIp = binary.BigEndian.Uint32(ip.To4())
}
requestId := r.getRequestId(r.serverUniqId, r.instanceUniqId, isClientIpv6, clientIp, clientPort)
requestId := r.getRequestId(r.serverUniqId, r.instanceUniqId, isClientIpv6, clientIp)
return requestId
}
func (r *DefaultRequestIdGenerator) getRequestId(serverUniqId uint16, instanceUniqId uint16, clientIpV6 bool, clientIp uint32, clientPort uint16) string {
func (r *DefaultRequestIdGenerator) getRequestId(serverUniqId uint16, instanceUniqId uint16, clientIpV6 bool, clientIp uint32) string {
clientIpv6Flag := uint32(0)
if clientIpV6 {
clientIpv6Flag = uint32(1)
}
// 128bits = serverUniqId(16bits) + instanceUniqId(16bits) + secondsElapsedToday(17bits) + clientPortLow15Bits(15bits) + sequentialNumber(30bits) + clientPortHigh1Bit(1bit) + clientIpv6Flag(1bit) + clientIp(32bits)
// 128bits = serverUniqId(16bits) + instanceUniqId(16bits) + secondsElapsedToday(17bits) + randomNumber(15bits) + sequentialNumber(31bits) + clientIpv6Flag(1bit) + clientIp(32bits)
secondsElapsedToday := r.getSecondsElapsedToday()
secondsLow17bits := uint32(secondsElapsedToday & secondsTodayBitsMask)
clientPortHigh1bit := uint32((clientPort & clientPortNumberHigh1BitMask) >> clientPortNumberLow15Bits)
clientPortLow15bits := uint32(clientPort & clientPortNumberLow15BitsMask)
randomNumber, _ := utils.GetRandomInteger(math.MaxInt16)
randomNumberLow15bits := uint32(randomNumber & randomNumberBitsMask)
secondsAndClientPortLowBits := (secondsLow17bits << clientPortNumberLow15Bits) | clientPortLow15bits
secondsAndRandomNumber := (secondsLow17bits << randomNumberBits) | randomNumberLow15bits
seqId := r.requestSeqId.Add(1)
seqIdLow30bits := seqId & reqSeqNumberBitsMask
seqIdLow31bits := seqId & reqSeqNumberBitsMask
seqIdAndClientPortHighBitAndClientIpv6Flag := (seqIdLow30bits << (clientPortNumberHigh1Bit + clientIpv6Bit)) | (clientPortHigh1bit << clientPortNumberHigh1Bit) | (clientIpv6Flag & clientIpv6BitMask)
seqIdAndClientIpv6Flag := (seqIdLow31bits << clientIpv6Bit) | (clientIpv6Flag & clientIpv6BitMask)
buf := &bytes.Buffer{}
_ = binary.Write(buf, binary.BigEndian, serverUniqId)
_ = binary.Write(buf, binary.BigEndian, instanceUniqId)
_ = binary.Write(buf, binary.BigEndian, secondsAndClientPortLowBits)
_ = binary.Write(buf, binary.BigEndian, seqIdAndClientPortHighBitAndClientIpv6Flag)
_ = binary.Write(buf, binary.BigEndian, secondsAndRandomNumber)
_ = binary.Write(buf, binary.BigEndian, seqIdAndClientIpv6Flag)
_ = binary.Write(buf, binary.BigEndian, clientIp)
return r.getUuidFromRequestId(buf)
@@ -200,25 +198,23 @@ func (r *DefaultRequestIdGenerator) parseRequestIdInfo(data []byte) *RequestIdIn
var serverUniqId uint16
var instanceUniqId uint16
var secondsAndClientPortLowBits uint32
var seqIdAndClientPortHighBitAndClientIpv6Flag uint32
var secondsAndRandomNumber uint32
var seqIdAndClientIpv6Flag uint32
var clientIp uint32
_ = binary.Read(buf, binary.BigEndian, &serverUniqId)
_ = binary.Read(buf, binary.BigEndian, &instanceUniqId)
_ = binary.Read(buf, binary.BigEndian, &secondsAndClientPortLowBits)
_ = binary.Read(buf, binary.BigEndian, &seqIdAndClientPortHighBitAndClientIpv6Flag)
_ = binary.Read(buf, binary.BigEndian, &secondsAndRandomNumber)
_ = binary.Read(buf, binary.BigEndian, &seqIdAndClientIpv6Flag)
_ = binary.Read(buf, binary.BigEndian, &clientIp)
secondsElapsedToday := (secondsAndClientPortLowBits >> clientPortNumberLow15Bits) & secondsTodayBitsMask
secondsElapsedToday := (secondsAndRandomNumber >> randomNumberBits) & secondsTodayBitsMask
randomNumber := (secondsAndRandomNumber & randomNumberBitsMask)
seqId := (seqIdAndClientPortHighBitAndClientIpv6Flag >> (clientPortNumberHigh1Bit + clientIpv6Bit)) & reqSeqNumberBitsMask
clientPortHigh1Bit := ((seqIdAndClientPortHighBitAndClientIpv6Flag >> clientIpv6Bit) << clientPortNumberLow15Bits) & clientPortNumberHigh1BitMask
isClientIpv6Flag := seqIdAndClientPortHighBitAndClientIpv6Flag & clientIpv6BitMask
seqId := (seqIdAndClientIpv6Flag >> clientIpv6Bit) & reqSeqNumberBitsMask
isClientIpv6Flag := (seqIdAndClientIpv6Flag & clientIpv6BitMask)
isClientIpv6 := false
clientPort := uint16(clientPortHigh1Bit | (secondsAndClientPortLowBits & clientPortNumberLow15BitsMask))
if isClientIpv6Flag == 1 {
isClientIpv6 = true
}
@@ -228,9 +224,9 @@ func (r *DefaultRequestIdGenerator) parseRequestIdInfo(data []byte) *RequestIdIn
InstanceUniqId: instanceUniqId,
SecondsElapsedToday: secondsElapsedToday,
RequestSeqId: seqId,
RandomNumber: randomNumber,
IsClientIpv6: isClientIpv6,
ClientIp: clientIp,
ClientPort: clientPort,
}
}
@@ -10,7 +10,7 @@ import (
func TestNewDefaultRequestIdGenerator_Http(t *testing.T) {
generator, _ := NewDefaultRequestIdGenerator(&settings.Config{HttpAddr: "123.234.123.234", HttpPort: 8080, SecretKey: "secretkey"})
requestId := generator.GenerateRequestId("127.0.0.1", 20000)
requestId := generator.GenerateRequestId("127.0.0.1")
requestIdInfo := generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedServerUniqId := uint16(0x2476) // crc32("123.234.123.234" + "_" + "secretkey") & 0xFFFF
@@ -24,7 +24,7 @@ func TestNewDefaultRequestIdGenerator_Http(t *testing.T) {
func TestNewDefaultRequestIdGenerator_UnixSocket(t *testing.T) {
generator, _ := NewDefaultRequestIdGenerator(&settings.Config{HttpAddr: "1.2.3.4", UnixSocketPath: "/var/lib/ezbookkeeping/ezbookkeeping.sock", Protocol: "socket", SecretKey: "secretkey"})
requestId := generator.GenerateRequestId("127.0.0.1", 20000)
requestId := generator.GenerateRequestId("127.0.0.1")
requestIdInfo := generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedServerUniqId := uint16(0x5bdb) // crc32("1.2.3.4" + "_" + "secretkey") & 0xFFFF
@@ -38,7 +38,7 @@ func TestNewDefaultRequestIdGenerator_UnixSocket(t *testing.T) {
func TestNewDefaultRequestIdGenerator_ClientIpv4(t *testing.T) {
generator, _ := NewDefaultRequestIdGenerator(&settings.Config{HttpAddr: "1.2.3.4", UnixSocketPath: "/var/lib/ezbookkeeping/ezbookkeeping.sock", Protocol: "socket", SecretKey: "secretkey"})
requestId := generator.GenerateRequestId("127.0.0.1", 20000)
requestId := generator.GenerateRequestId("127.0.0.1")
requestIdInfo := generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientIp := uint32(0x7f000001) // 127.0.0.1
@@ -49,7 +49,7 @@ func TestNewDefaultRequestIdGenerator_ClientIpv4(t *testing.T) {
actualClientIpv6 := requestIdInfo.IsClientIpv6
assert.Equal(t, expectedClientIpv6, actualClientIpv6)
requestId = generator.GenerateRequestId("192.168.1.100", 20000)
requestId = generator.GenerateRequestId("192.168.1.100")
requestIdInfo = generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientIp = uint32(0xc0a80164) // 192.168.1.100
@@ -63,7 +63,7 @@ func TestNewDefaultRequestIdGenerator_ClientIpv4(t *testing.T) {
func TestNewDefaultRequestIdGenerator_ClientIpv6(t *testing.T) {
generator, _ := NewDefaultRequestIdGenerator(&settings.Config{HttpAddr: "1.2.3.4", UnixSocketPath: "/var/lib/ezbookkeeping/ezbookkeeping.sock", Protocol: "socket", SecretKey: "secretkey"})
requestId := generator.GenerateRequestId("2001:abc:def:1234::1", 20000)
requestId := generator.GenerateRequestId("2001:abc:def:1234::1")
requestIdInfo := generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientIp := uint32(0x76fe1b98) // crc32("2001:abc:def:1234::1")
@@ -74,7 +74,7 @@ func TestNewDefaultRequestIdGenerator_ClientIpv6(t *testing.T) {
actualClientIpv6 := requestIdInfo.IsClientIpv6
assert.Equal(t, expectedClientIpv6, actualClientIpv6)
requestId = generator.GenerateRequestId("2400:abcd:1234:1:56ef:ab78:c90d:1e2f", 20000)
requestId = generator.GenerateRequestId("2400:abcd:1234:1:56ef:ab78:c90d:1e2f")
requestIdInfo = generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientIp = uint32(0xa0a25faa) // crc32("2400:abcd:1234:1:56ef:ab78:c90d:1e2f")
@@ -86,56 +86,11 @@ func TestNewDefaultRequestIdGenerator_ClientIpv6(t *testing.T) {
assert.Equal(t, expectedClientIpv6, actualClientIpv6)
}
func TestNewDefaultRequestIdGenerator_ClientPort(t *testing.T) {
generator, _ := NewDefaultRequestIdGenerator(&settings.Config{HttpAddr: "1.2.3.4", UnixSocketPath: "/var/lib/ezbookkeeping/ezbookkeeping.sock", Protocol: "socket", SecretKey: "secretkey"})
requestId := generator.GenerateRequestId("127.0.0.1", 0)
requestIdInfo := generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientPort := uint16(0)
actualClientPort := requestIdInfo.ClientPort
assert.Equal(t, expectedClientPort, actualClientPort)
requestId = generator.GenerateRequestId("127.0.0.1", 12345)
requestIdInfo = generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientPort = uint16(12345)
actualClientPort = requestIdInfo.ClientPort
assert.Equal(t, expectedClientPort, actualClientPort)
requestId = generator.GenerateRequestId("127.0.0.1", 32767)
requestIdInfo = generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientPort = uint16(32767)
actualClientPort = requestIdInfo.ClientPort
assert.Equal(t, expectedClientPort, actualClientPort)
requestId = generator.GenerateRequestId("127.0.0.1", 32768)
requestIdInfo = generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientPort = uint16(32768)
actualClientPort = requestIdInfo.ClientPort
assert.Equal(t, expectedClientPort, actualClientPort)
requestId = generator.GenerateRequestId("127.0.0.1", 56789)
requestIdInfo = generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientPort = uint16(56789)
actualClientPort = requestIdInfo.ClientPort
assert.Equal(t, expectedClientPort, actualClientPort)
requestId = generator.GenerateRequestId("127.0.0.1", 65535)
requestIdInfo = generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedClientPort = uint16(65535)
actualClientPort = requestIdInfo.ClientPort
assert.Equal(t, expectedClientPort, actualClientPort)
}
func TestGenerateRequestId_100Times(t *testing.T) {
generator, _ := NewDefaultRequestIdGenerator(&settings.Config{HttpAddr: "1.2.3.4", HttpPort: 1234, SecretKey: "secretkey"})
for i := 1; i <= 100; i++ {
requestId := generator.GenerateRequestId("127.0.0.1", 20000)
requestId := generator.GenerateRequestId("127.0.0.1")
requestIdInfo := generator.parseRequestIdInfo(generator.parseRequestIdFromUuid(requestId))
expectedRequestSeqId := uint32(i)
+2 -2
View File
@@ -27,6 +27,6 @@ func InitializeRequestIdGenerator(config *settings.Config) error {
}
// GenerateRequestId returns a new request id by the current request id generator
func (u *RequestIdContainer) GenerateRequestId(clientIpAddr string, clientPort uint16) string {
return u.Current.GenerateRequestId(clientIpAddr, clientPort)
func (u *RequestIdContainer) GenerateRequestId(clientIpAddr string) string {
return u.Current.GenerateRequestId(clientIpAddr)
}
+1 -1
View File
@@ -2,7 +2,7 @@ package requestid
// RequestIdGenerator is common request generator interface
type RequestIdGenerator interface {
GenerateRequestId(clientIpAddr string, clientPort uint16) string
GenerateRequestId(clientIpAddr string) string
GetCurrentServerUniqId() uint16
GetCurrentInstanceUniqId() uint16
}
+3 -46
View File
@@ -1,7 +1,6 @@
package services
import (
"strings"
"time"
"xorm.io/xorm"
@@ -55,7 +54,7 @@ func (s *AccountService) GetAllAccountsByUid(c *core.Context, uid int64) ([]*mod
return accounts, err
}
// GetAccountAndSubAccountsByAccountId returns account model and sub-account models according to account id
// GetAccountAndSubAccountsByAccountId returns account model and sub account models according to account id
func (s *AccountService) GetAccountAndSubAccountsByAccountId(c *core.Context, uid int64, accountId int64) ([]*models.Account, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
@@ -71,7 +70,7 @@ func (s *AccountService) GetAccountAndSubAccountsByAccountId(c *core.Context, ui
return accounts, err
}
// GetSubAccountsByAccountId returns sub-account models according to account id
// GetSubAccountsByAccountId returns sub account models according to account id
func (s *AccountService) GetSubAccountsByAccountId(c *core.Context, uid int64, accountId int64) ([]*models.Account, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
@@ -87,48 +86,6 @@ func (s *AccountService) GetSubAccountsByAccountId(c *core.Context, uid int64, a
return accounts, err
}
// GetSubAccountsByAccountIds returns sub-account models according to account ids
func (s *AccountService) GetSubAccountsByAccountIds(c *core.Context, uid int64, accountIds []int64) ([]*models.Account, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
if len(accountIds) <= 0 {
return nil, errs.ErrAccountIdInvalid
}
condition := "uid=? AND deleted=?"
conditionParams := make([]any, 0, len(accountIds)+2)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
var accountIdConditions strings.Builder
for i := 0; i < len(accountIds); i++ {
if accountIds[i] <= 0 {
return nil, errs.ErrAccountIdInvalid
}
if accountIdConditions.Len() > 0 {
accountIdConditions.WriteString(",")
}
accountIdConditions.WriteString("?")
conditionParams = append(conditionParams, accountIds[i])
}
if accountIdConditions.Len() > 1 {
condition = condition + " AND parent_account_id IN (" + accountIdConditions.String() + ")"
} else {
condition = condition + " AND parent_account_id = " + accountIdConditions.String()
}
var accounts []*models.Account
err := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).OrderBy("display_order asc").Find(&accounts)
return accounts, err
}
// GetAccountsByAccountIds returns account models according to account ids
func (s *AccountService) GetAccountsByAccountIds(c *core.Context, uid int64, accountIds []int64) (map[int64]*models.Account, error) {
if uid <= 0 {
@@ -170,7 +127,7 @@ func (s *AccountService) GetMaxDisplayOrder(c *core.Context, uid int64, category
}
}
// GetMaxSubAccountDisplayOrder returns the max display order of sub-account according to account category and parent account id
// GetMaxSubAccountDisplayOrder returns the max display order of sub account according to account category and parent account id
func (s *AccountService) GetMaxSubAccountDisplayOrder(c *core.Context, uid int64, category models.AccountCategory, parentAccountId int64) (int32, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
+7 -33
View File
@@ -58,7 +58,7 @@ func (s *TokenService) GetAllUnexpiredNormalTokensByUid(c *core.Context, uid int
now := time.Now().Unix()
var tokenRecords []*models.TokenRecord
err := s.TokenDB(uid).NewSession(c).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time", "last_seen_unix_time").Where("uid=? AND token_type=? AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, now).Find(&tokenRecords)
err := s.TokenDB(uid).NewSession(c).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time").Where("uid=? AND token_type=? AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, now).Find(&tokenRecords)
return tokenRecords, err
}
@@ -98,31 +98,6 @@ func (s *TokenService) CreatePasswordResetToken(c *core.Context, user *models.Us
return s.createToken(c, user, core.USER_TOKEN_TYPE_PASSWORD_RESET, s.getUserAgent(c), s.CurrentConfig().PasswordResetTokenExpiredTimeDuration)
}
// UpdateTokenLastSeen updates the last seen time of specified token
func (s *TokenService) UpdateTokenLastSeen(c *core.Context, tokenRecord *models.TokenRecord) error {
if tokenRecord.Uid <= 0 {
return errs.ErrUserIdInvalid
}
if tokenRecord.UserTokenId <= 0 {
return errs.ErrInvalidUserTokenId
}
tokenRecord.LastSeenUnixTime = time.Now().Unix()
return s.TokenDB(tokenRecord.Uid).DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.Cols("last_seen_unix_time").Where("uid=? AND user_token_id=? AND created_unix_time=?", tokenRecord.Uid, tokenRecord.UserTokenId, tokenRecord.CreatedUnixTime).Update(tokenRecord)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrTokenRecordNotFound
}
return nil
})
}
// DeleteToken deletes given token from database
func (s *TokenService) DeleteToken(c *core.Context, tokenRecord *models.TokenRecord) error {
if tokenRecord.Uid <= 0 {
@@ -319,13 +294,12 @@ func (s *TokenService) createToken(c *core.Context, user *models.User, tokenType
now := time.Now()
tokenRecord := &models.TokenRecord{
Uid: user.Uid,
UserTokenId: s.getUserTokenId(),
TokenType: tokenType,
UserAgent: userAgent,
CreatedUnixTime: now.Unix(),
ExpiredUnixTime: now.Add(expiryDate).Unix(),
LastSeenUnixTime: now.Unix(),
Uid: user.Uid,
UserTokenId: s.getUserTokenId(),
TokenType: tokenType,
UserAgent: userAgent,
CreatedUnixTime: now.Unix(),
ExpiredUnixTime: now.Add(expiryDate).Unix(),
}
if tokenRecord.Secret, err = utils.GetRandomString(10); err != nil {
+1 -44
View File
@@ -1,7 +1,6 @@
package services
import (
"strings"
"time"
"xorm.io/xorm"
@@ -69,48 +68,6 @@ func (s *TransactionCategoryService) GetAllCategoriesByUid(c *core.Context, uid
return categories, err
}
// GetSubCategoriesByCategoryIds returns sub-category models according to category ids
func (s *TransactionCategoryService) GetSubCategoriesByCategoryIds(c *core.Context, uid int64, categoryIds []int64) ([]*models.TransactionCategory, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
if len(categoryIds) <= 0 {
return nil, errs.ErrTransactionCategoryIdInvalid
}
condition := "uid=? AND deleted=?"
conditionParams := make([]any, 0, len(categoryIds)+2)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
var categoryIdConditions strings.Builder
for i := 0; i < len(categoryIds); i++ {
if categoryIds[i] <= 0 {
return nil, errs.ErrTransactionCategoryIdInvalid
}
if categoryIdConditions.Len() > 0 {
categoryIdConditions.WriteString(",")
}
categoryIdConditions.WriteString("?")
conditionParams = append(conditionParams, categoryIds[i])
}
if categoryIdConditions.Len() > 1 {
condition = condition + " AND parent_category_id IN (" + categoryIdConditions.String() + ")"
} else {
condition = condition + " AND parent_category_id = " + categoryIdConditions.String()
}
var categories []*models.TransactionCategory
err := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).OrderBy("display_order asc").Find(&categories)
return categories, err
}
// GetCategoryByCategoryId returns a transaction category model according to transaction category id
func (s *TransactionCategoryService) GetCategoryByCategoryId(c *core.Context, uid int64, categoryId int64) (*models.TransactionCategory, error) {
if uid <= 0 {
@@ -292,7 +249,7 @@ func (s *TransactionCategoryService) ModifyCategory(c *core.Context, category *m
category.UpdatedUnixTime = time.Now().Unix()
return s.UserDataDB(category.Uid).DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.ID(category.CategoryId).Cols("parent_category_id", "name", "icon", "color", "comment", "hidden", "updated_unix_time").Where("uid=? AND deleted=?", category.Uid, false).Update(category)
updatedRows, err := sess.ID(category.CategoryId).Cols("name", "icon", "color", "comment", "hidden", "updated_unix_time").Where("uid=? AND deleted=?", category.Uid, false).Update(category)
if err != nil {
return err
+10 -83
View File
@@ -12,8 +12,6 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/uuid"
)
const pageCountForLoadAllTransactionTagIndexes = 1000
// TransactionTagService represents transaction tag service
type TransactionTagService struct {
ServiceUsingDB
@@ -119,60 +117,15 @@ func (s *TransactionTagService) GetMaxDisplayOrder(c *core.Context, uid int64) (
}
// GetAllTagIdsOfAllTransactions returns all transaction tag ids
func (s *TransactionTagService) GetAllTagIdsOfAllTransactions(c *core.Context, uid int64) ([]*models.TransactionTagIndex, error) {
func (s *TransactionTagService) GetAllTagIdsOfAllTransactions(c *core.Context, uid int64) (map[int64][]int64, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
condition := "uid=? AND deleted=?"
conditionParams := make([]any, 0, 2)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
var tagIndexs []*models.TransactionTagIndex
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).Find(&tagIndexs)
var allTransactionTagIndexes []*models.TransactionTagIndex
maxTransactionTagIndexId := int64(0)
for maxTransactionTagIndexId >= 0 {
var tagIndexes []*models.TransactionTagIndex
finalCondition := condition
finalConditionParams := make([]any, 0, 3)
finalConditionParams = append(finalConditionParams, conditionParams...)
if maxTransactionTagIndexId > 0 {
finalCondition = finalCondition + " AND tag_index_id<=?"
finalConditionParams = append(finalConditionParams, maxTransactionTagIndexId)
}
err := s.UserDataDB(uid).NewSession(c).Where(finalCondition, finalConditionParams...).Limit(pageCountForLoadAllTransactionTagIndexes, 0).OrderBy("tag_index_id desc").Find(&tagIndexes)
if err != nil {
return nil, err
}
allTransactionTagIndexes = append(allTransactionTagIndexes, tagIndexes...)
if len(tagIndexes) < pageCountForLoadAllTransactionTagIndexes {
maxTransactionTagIndexId = -1
break
}
maxTransactionTagIndexId = tagIndexes[len(tagIndexes)-1].TagIndexId - 1
}
return allTransactionTagIndexes, nil
}
// GetAllTagIdsMapOfAllTransactions returns all transaction tag ids map grouped by transaction id
func (s *TransactionTagService) GetAllTagIdsMapOfAllTransactions(c *core.Context, uid int64) (map[int64][]int64, error) {
tagIndexes, err := s.GetAllTagIdsOfAllTransactions(c, uid)
if err != nil {
return nil, err
}
allTransactionTagIds := s.GetGroupedTransactionTagIds(tagIndexes)
allTransactionTagIds := s.getGroupedTransactionTagIds(tagIndexs)
return allTransactionTagIds, err
}
@@ -183,10 +136,10 @@ func (s *TransactionTagService) GetAllTagIdsOfTransactions(c *core.Context, uid
return nil, errs.ErrUserIdInvalid
}
var tagIndexes []*models.TransactionTagIndex
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).In("transaction_id", transactionIds).OrderBy("transaction_id asc, tag_index_id asc").Find(&tagIndexes)
var tagIndexs []*models.TransactionTagIndex
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).In("transaction_id", transactionIds).Find(&tagIndexs)
allTransactionTagIds := s.GetGroupedTransactionTagIds(tagIndexes)
allTransactionTagIds := s.getGroupedTransactionTagIds(tagIndexs)
return allTransactionTagIds, err
}
@@ -377,32 +330,6 @@ func (s *TransactionTagService) ExistsTagName(c *core.Context, uid int64, name s
return s.UserDataDB(uid).NewSession(c).Cols("name").Where("uid=? AND deleted=? AND name=?", uid, false, name).Exist(&models.TransactionTag{})
}
// ModifyTagIndexTransactionTime updates transaction time of given transaction tag indexes
func (s *TransactionTagService) ModifyTagIndexTransactionTime(c *core.Context, uid int64, tagIndexes []*models.TransactionTagIndex) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
for i := 0; i < len(tagIndexes); i++ {
tagIndexes[i].UpdatedUnixTime = time.Now().Unix()
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
for i := 0; i < len(tagIndexes); i++ {
tagIndex := tagIndexes[i]
updatedRows, err := sess.ID(tagIndex.TagIndexId).Cols("transaction_time", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(tagIndex)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrTransactionTagIndexNotFound
}
}
return nil
})
}
// GetTagMapByList returns a transaction tag map by a list
func (s *TransactionTagService) GetTagMapByList(tags []*models.TransactionTag) map[int64]*models.TransactionTag {
tagMap := make(map[int64]*models.TransactionTag)
@@ -414,11 +341,11 @@ func (s *TransactionTagService) GetTagMapByList(tags []*models.TransactionTag) m
return tagMap
}
func (s *TransactionTagService) GetGroupedTransactionTagIds(tagIndexes []*models.TransactionTagIndex) map[int64][]int64 {
func (s *TransactionTagService) getGroupedTransactionTagIds(tagIndexs []*models.TransactionTagIndex) map[int64][]int64 {
allTransactionTagIds := make(map[int64][]int64)
for i := 0; i < len(tagIndexes); i++ {
tagIndex := tagIndexes[i]
for i := 0; i < len(tagIndexs); i++ {
tagIndex := tagIndexs[i]
var transactionTagIds []int64
-240
View File
@@ -1,240 +0,0 @@
package services
import (
"time"
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/uuid"
)
// TransactionTemplateService represents transaction template service
type TransactionTemplateService struct {
ServiceUsingDB
ServiceUsingUuid
}
// Initialize a transaction template service singleton instance
var (
TransactionTemplates = &TransactionTemplateService{
ServiceUsingDB: ServiceUsingDB{
container: datastore.Container,
},
ServiceUsingUuid: ServiceUsingUuid{
container: uuid.Container,
},
}
)
// GetTotalNormalTemplateCountByUid returns total template count of user
func (s *TransactionTemplateService) GetTotalNormalTemplateCountByUid(c *core.Context, uid int64) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
count, err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=? AND template_type=?", uid, false, models.TRANSACTION_TEMPLATE_TYPE_NORMAL).Count(&models.TransactionTemplate{})
return count, err
}
// GetAllTemplatesByUid returns all transaction template models of user
func (s *TransactionTemplateService) GetAllTemplatesByUid(c *core.Context, uid int64, templateType models.TransactionTemplateType) ([]*models.TransactionTemplate, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
var templates []*models.TransactionTemplate
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=? AND template_type=?", uid, false, templateType).Find(&templates)
return templates, err
}
// GetTemplateByTemplateId returns a transaction template model according to transaction template id
func (s *TransactionTemplateService) GetTemplateByTemplateId(c *core.Context, uid int64, templateId int64) (*models.TransactionTemplate, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
if templateId <= 0 {
return nil, errs.ErrTransactionTemplateIdInvalid
}
template := &models.TransactionTemplate{}
has, err := s.UserDataDB(uid).NewSession(c).ID(templateId).Where("uid=? AND deleted=?", uid, false).Get(template)
if err != nil {
return nil, err
} else if !has {
return nil, errs.ErrTransactionTemplateNotFound
}
return template, nil
}
// GetMaxDisplayOrder returns the max display order
func (s *TransactionTemplateService) GetMaxDisplayOrder(c *core.Context, uid int64, templateType models.TransactionTemplateType) (int32, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
template := &models.TransactionTemplate{}
has, err := s.UserDataDB(uid).NewSession(c).Cols("uid", "deleted", "display_order").Where("uid=? AND deleted=? AND template_type=?", uid, false, templateType).OrderBy("display_order desc").Limit(1).Get(template)
if err != nil {
return 0, err
}
if has {
return template.DisplayOrder, nil
} else {
return 0, nil
}
}
// CreateTemplate saves a new transaction template model to database
func (s *TransactionTemplateService) CreateTemplate(c *core.Context, template *models.TransactionTemplate) error {
if template.Uid <= 0 {
return errs.ErrUserIdInvalid
}
template.TemplateId = s.GenerateUuid(uuid.UUID_TYPE_TEMPLATE)
if template.TemplateId < 1 {
return errs.ErrSystemIsBusy
}
template.Deleted = false
template.CreatedUnixTime = time.Now().Unix()
template.UpdatedUnixTime = time.Now().Unix()
return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Insert(template)
return err
})
}
// ModifyTemplate saves an existed transaction template model to database
func (s *TransactionTemplateService) ModifyTemplate(c *core.Context, template *models.TransactionTemplate) error {
if template.Uid <= 0 {
return errs.ErrUserIdInvalid
}
template.UpdatedUnixTime = time.Now().Unix()
return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.ID(template.TemplateId).Cols("name", "type", "category_id", "account_id", "tag_ids", "amount", "related_account_id", "related_account_amount", "hide_amount", "comment", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrTransactionTemplateNotFound
}
return err
})
}
// HideTemplate updates hidden field of given transaction templates
func (s *TransactionTemplateService) HideTemplate(c *core.Context, uid int64, ids []int64, hidden bool) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
now := time.Now().Unix()
updateModel := &models.TransactionTemplate{
Hidden: hidden,
UpdatedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.Cols("hidden", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).In("template_id", ids).Update(updateModel)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrTransactionTemplateNotFound
}
return err
})
}
// ModifyTemplateDisplayOrders updates display order of given transaction templates
func (s *TransactionTemplateService) ModifyTemplateDisplayOrders(c *core.Context, uid int64, templates []*models.TransactionTemplate) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
for i := 0; i < len(templates); i++ {
templates[i].UpdatedUnixTime = time.Now().Unix()
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
for i := 0; i < len(templates); i++ {
template := templates[i]
updatedRows, err := sess.ID(template.TemplateId).Cols("display_order", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(template)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrTransactionTemplateNotFound
}
}
return nil
})
}
// DeleteTemplate deletes an existed transaction template from database
func (s *TransactionTemplateService) DeleteTemplate(c *core.Context, uid int64, templateId int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
now := time.Now().Unix()
updateModel := &models.TransactionTemplate{
Deleted: true,
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
deletedRows, err := sess.ID(templateId).Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel)
if err != nil {
return err
} else if deletedRows < 1 {
return errs.ErrTransactionTemplateNotFound
}
return err
})
}
// DeleteAllTemplates deletes all existed transaction templates from database
func (s *TransactionTemplateService) DeleteAllTemplates(c *core.Context, uid int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
now := time.Now().Unix()
updateModel := &models.TransactionTemplate{
Deleted: true,
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel)
if err != nil {
return err
}
return nil
})
}
+145 -541
View File
@@ -5,7 +5,6 @@ import (
"strings"
"time"
"xorm.io/builder"
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
@@ -16,8 +15,6 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/uuid"
)
const pageCountForLoadTransactionAmounts = 1000
// TransactionService represents transaction service
type TransactionService struct {
ServiceUsingDB
@@ -74,11 +71,11 @@ func (s *TransactionService) GetAllTransactions(c *core.Context, uid int64, page
// GetAllTransactionsByMaxTime returns all transactions before given time
func (s *TransactionService) GetAllTransactionsByMaxTime(c *core.Context, uid int64, maxTransactionTime int64, count int32, noDuplicated bool) ([]*models.Transaction, error) {
return s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, "", "", 1, count, false, noDuplicated)
return s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, "", 1, count, false, noDuplicated)
}
// GetTransactionsByMaxTime returns transactions before given time
func (s *TransactionService) GetTransactionsByMaxTime(c *core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, amountFilter string, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) {
func (s *TransactionService) GetTransactionsByMaxTime(c *core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -102,44 +99,34 @@ func (s *TransactionService) GetTransactionsByMaxTime(c *core.Context, uid int64
actualCount++
}
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagIds, amountFilter, keyword, noDuplicated)
sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...)
if len(tagIds) > 0 {
sess.In("transaction_id", s.getTransactionQueryByTagIdsCondition(uid, maxTransactionTime, minTransactionTime, tagIds))
} else if noTags {
sess.NotIn("transaction_id", s.getTransactionQueryByAllTagIdsCondition(uid, maxTransactionTime, minTransactionTime))
}
err = sess.Limit(int(actualCount), int(count*(page-1))).OrderBy("transaction_time desc").Find(&transactions)
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword, noDuplicated)
err = s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).Limit(int(actualCount), int(count*(page-1))).OrderBy("transaction_time desc").Find(&transactions)
return transactions, err
}
// GetTransactionsInMonthByPage returns all transactions in given year and month
func (s *TransactionService) GetTransactionsInMonthByPage(c *core.Context, uid int64, year int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, amountFilter string, keyword string) ([]*models.Transaction, error) {
func (s *TransactionService) GetTransactionsInMonthByPage(c *core.Context, uid int64, year int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string) ([]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
minTransactionTime, maxTransactionTime, err := utils.GetTransactionTimeRangeByYearMonth(year, month)
startMinUnixTime, err := utils.ParseFromLongDateTimeToMinUnixTime(fmt.Sprintf("%d-%02d-01 00:00:00", year, month))
startMaxUnixTime, err := utils.ParseFromLongDateTimeToMaxUnixTime(fmt.Sprintf("%d-%02d-01 00:00:00", year, month))
if err != nil {
return nil, errs.ErrSystemError
}
endMaxUnixTime := startMaxUnixTime.AddDate(0, 1, 0)
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startMinUnixTime.Unix())
maxTransactionTime := utils.GetMinTransactionTimeFromUnixTime(endMaxUnixTime.Unix()) - 1
var transactions []*models.Transaction
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagIds, amountFilter, keyword, true)
sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...)
if len(tagIds) > 0 {
sess.In("transaction_id", s.getTransactionQueryByTagIdsCondition(uid, maxTransactionTime, minTransactionTime, tagIds))
} else if noTags {
sess.NotIn("transaction_id", s.getTransactionQueryByAllTagIdsCondition(uid, maxTransactionTime, minTransactionTime))
}
err = sess.OrderBy("transaction_time desc").Find(&transactions)
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword, true)
err = s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).OrderBy("transaction_time desc").Find(&transactions)
transactionsInMonth := make([]*models.Transaction, 0, len(transactions))
@@ -180,25 +167,37 @@ func (s *TransactionService) GetTransactionByTransactionId(c *core.Context, uid
// GetAllTransactionCount returns total count of transactions
func (s *TransactionService) GetAllTransactionCount(c *core.Context, uid int64) (int64, error) {
return s.GetTransactionCount(c, uid, 0, 0, 0, nil, nil, nil, false, "", "")
return s.GetTransactionCount(c, uid, 0, 0, 0, nil, nil, "")
}
// GetTransactionCount returns count of transactions
func (s *TransactionService) GetTransactionCount(c *core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, amountFilter string, keyword string) (int64, error) {
// GetMonthTransactionCount returns total count of transactions in given year and month
func (s *TransactionService) GetMonthTransactionCount(c *core.Context, uid int64, year int32, month int32, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string, utcOffset int16) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagIds, amountFilter, keyword, true)
sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...)
startTime, err := utils.ParseFromLongDateTime(fmt.Sprintf("%d-%02d-01 00:00:00", year, month), utcOffset)
if len(tagIds) > 0 {
sess.In("transaction_id", s.getTransactionQueryByTagIdsCondition(uid, maxTransactionTime, minTransactionTime, tagIds))
} else if noTags {
sess.NotIn("transaction_id", s.getTransactionQueryByAllTagIdsCondition(uid, maxTransactionTime, minTransactionTime))
if err != nil {
return 0, errs.ErrSystemError
}
return sess.Count(&models.Transaction{})
endTime := startTime.AddDate(0, 1, 0)
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startTime.Unix())
maxTransactionTime := utils.GetMinTransactionTimeFromUnixTime(endTime.Unix()) - 1
return s.GetTransactionCount(c, uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword)
}
// GetTransactionCount returns count of transactions
func (s *TransactionService) GetTransactionCount(c *core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword, true)
return s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...).Count(&models.Transaction{})
}
// CreateTransaction saves a new transaction to database
@@ -240,7 +239,7 @@ func (s *TransactionService) CreateTransaction(c *core.Context, transaction *mod
transaction.UpdatedUnixTime = now
tagIds = utils.ToUniqueInt64Slice(tagIds)
transactionTagIndexes := make([]*models.TransactionTagIndex, len(tagIds))
transactionTagIndexs := make([]*models.TransactionTagIndex, len(tagIds))
for i := 0; i < len(tagIds); i++ {
tagIndexId := s.GenerateUuid(uuid.UUID_TYPE_TAG_INDEX)
@@ -249,7 +248,7 @@ func (s *TransactionService) CreateTransaction(c *core.Context, transaction *mod
return errs.ErrSystemIsBusy
}
transactionTagIndexes[i] = &models.TransactionTagIndex{
transactionTagIndexs[i] = &models.TransactionTagIndex{
TagIndexId: tagIndexId,
Uid: transaction.Uid,
Deleted: false,
@@ -272,10 +271,6 @@ func (s *TransactionService) CreateTransaction(c *core.Context, transaction *mod
return errs.ErrCannotAddTransactionToHiddenAccount
}
if sourceAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS || (destinationAccount != nil && destinationAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS) {
return errs.ErrCannotAddTransactionToParentAccount
}
if (transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN) &&
sourceAccount.Currency == destinationAccount.Currency && transaction.Amount != transaction.RelatedAccountAmount {
return errs.ErrTransactionSourceAndDestinationAmountNotEqual
@@ -289,7 +284,7 @@ func (s *TransactionService) CreateTransaction(c *core.Context, transaction *mod
}
// Get and verify tags
err = s.isTagsValid(sess, transaction, transactionTagIndexes, tagIds)
err = s.isTagsValid(sess, transaction, transactionTagIndexs, tagIds)
if err != nil {
return err
@@ -362,11 +357,9 @@ func (s *TransactionService) CreateTransaction(c *core.Context, transaction *mod
err = nil
// Insert transaction tag index
if len(transactionTagIndexes) > 0 {
for i := 0; i < len(transactionTagIndexes); i++ {
transactionTagIndex := transactionTagIndexes[i]
transactionTagIndex.TransactionTime = transaction.TransactionTime
if len(transactionTagIndexs) > 0 {
for i := 0; i < len(transactionTagIndexs); i++ {
transactionTagIndex := transactionTagIndexs[i]
_, err := sess.Insert(transactionTagIndex)
if err != nil {
@@ -430,7 +423,7 @@ func (s *TransactionService) CreateTransaction(c *core.Context, transaction *mod
}
// ModifyTransaction saves an existed transaction to database
func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *models.Transaction, currentTagIdsCount int, addTagIds []int64, removeTagIds []int64) error {
func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *models.Transaction, addTagIds []int64, removeTagIds []int64) error {
if transaction.Uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -446,7 +439,7 @@ func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *mod
addTagIds = utils.ToUniqueInt64Slice(addTagIds)
removeTagIds = utils.ToUniqueInt64Slice(removeTagIds)
transactionTagIndexes := make([]*models.TransactionTagIndex, len(addTagIds))
transactionTagIndexs := make([]*models.TransactionTagIndex, len(addTagIds))
for i := 0; i < len(addTagIds); i++ {
tagIndexId := s.GenerateUuid(uuid.UUID_TYPE_TAG_INDEX)
@@ -455,7 +448,7 @@ func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *mod
return errs.ErrSystemIsBusy
}
transactionTagIndexes[i] = &models.TransactionTagIndex{
transactionTagIndexs[i] = &models.TransactionTagIndex{
TagIndexId: tagIndexId,
Uid: transaction.Uid,
Deleted: false,
@@ -501,10 +494,6 @@ func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *mod
return errs.ErrCannotModifyTransactionInHiddenAccount
}
if sourceAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS || (destinationAccount != nil && destinationAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS) {
return errs.ErrCannotModifyTransactionInParentAccount
}
if (transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN) &&
sourceAccount.Currency == destinationAccount.Currency && transaction.Amount != transaction.RelatedAccountAmount {
return errs.ErrTransactionSourceAndDestinationAmountNotEqual
@@ -532,8 +521,6 @@ func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *mod
updateCols = append(updateCols, "category_id")
}
modifyTransactionTime := false
if utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime) != utils.GetUnixTimeFromTransactionTime(oldTransaction.TransactionTime) {
sameSecondLatestTransaction := &models.Transaction{}
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
@@ -552,7 +539,6 @@ func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *mod
}
updateCols = append(updateCols, "transaction_time")
modifyTransactionTime = true
}
if transaction.TimezoneUtcOffset != oldTransaction.TimezoneUtcOffset {
@@ -600,7 +586,7 @@ func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *mod
}
// Get and verify tags
err = s.isTagsValid(sess, transaction, transactionTagIndexes, addTagIds)
err = s.isTagsValid(sess, transaction, transactionTagIndexs, addTagIds)
if err != nil {
return err
@@ -648,27 +634,15 @@ func (s *TransactionService) ModifyTransaction(c *core.Context, transaction *mod
}
}
if len(transactionTagIndexes) > 0 {
for i := 0; i < len(transactionTagIndexes); i++ {
transactionTagIndex := transactionTagIndexes[i]
transactionTagIndex.TransactionTime = transaction.TransactionTime
if len(transactionTagIndexs) > 0 {
for i := 0; i < len(transactionTagIndexs); i++ {
transactionTagIndex := transactionTagIndexs[i]
_, err := sess.Insert(transactionTagIndex)
if err != nil {
return err
}
}
} else if len(transactionTagIndexes) == 0 && currentTagIdsCount > 0 && modifyTransactionTime {
tagIndexUpdateModel := &models.TransactionTagIndex{
TransactionTime: transaction.TransactionTime,
}
_, err := sess.Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, transaction.TransactionId).Update(tagIndexUpdateModel)
if err != nil {
return err
}
}
// Update account table
@@ -865,10 +839,6 @@ func (s *TransactionService) DeleteTransaction(c *core.Context, uid int64, trans
return errs.ErrCannotDeleteTransactionInHiddenAccount
}
if sourceAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS || (destinationAccount != nil && destinationAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS) {
return errs.ErrCannotDeleteTransactionInParentAccount
}
// Update transaction row to deleted
deletedRows, err := sess.ID(oldTransaction.TransactionId).Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel)
@@ -1040,27 +1010,45 @@ func (s *TransactionService) GetRelatedTransferTransaction(originalTransaction *
}
// GetAccountsTotalIncomeAndExpense returns the every accounts total income and expense amount by specific date range
func (s *TransactionService) GetAccountsTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64, utcOffset int16, useTransactionTimezone bool) (map[int64]int64, map[int64]int64, error) {
func (s *TransactionService) GetAccountsTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64) (map[int64]int64, map[int64]int64, error) {
if uid <= 0 {
return nil, nil, errs.ErrUserIdInvalid
}
clientLocation := time.FixedZone("Client Timezone", int(utcOffset)*60)
startLocalDateTime := utils.FormatUnixTimeToNumericLocalDateTime(startUnixTime, clientLocation)
endLocalDateTime := utils.FormatUnixTimeToNumericLocalDateTime(endUnixTime, clientLocation)
startUnixTime = utils.GetMinUnixTimeWithSameLocalDateTime(startUnixTime, utcOffset)
endUnixTime = utils.GetMaxUnixTimeWithSameLocalDateTime(endUnixTime, utcOffset)
startTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startUnixTime)
endTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(endUnixTime)
condition := "uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?"
conditionParams := make([]any, 0, 4)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
var transactionTotalAmounts []*models.Transaction
err := s.UserDataDB(uid).NewSession(c).Select("type, account_id, SUM(amount) as amount").Where("uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?", uid, false, models.TRANSACTION_DB_TYPE_INCOME, models.TRANSACTION_DB_TYPE_EXPENSE, startTransactionTime, endTransactionTime).GroupBy("type, account_id").Find(&transactionTotalAmounts)
if err != nil {
return nil, nil, err
}
incomeAmounts := make(map[int64]int64)
expenseAmounts := make(map[int64]int64)
for i := 0; i < len(transactionTotalAmounts); i++ {
transactionTotalAmount := transactionTotalAmounts[i]
if transactionTotalAmount.Type == models.TRANSACTION_DB_TYPE_INCOME {
incomeAmounts[transactionTotalAmount.AccountId] = transactionTotalAmount.Amount
} else if transactionTotalAmount.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
expenseAmounts[transactionTotalAmount.AccountId] = transactionTotalAmount.Amount
}
}
return incomeAmounts, expenseAmounts, nil
}
// GetAccountsMonthTotalIncomeAndExpense returns the every accounts total income and expense amount in month by specific date range
func (s *TransactionService) GetAccountsMonthTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64, pageCount int) (map[string]models.TransactionAccountsAmount, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
startTransactionTime := utils.GetMinTransactionTimeFromUnixTime(startUnixTime)
endTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(endUnixTime)
minTransactionTime := startTransactionTime
maxTransactionTime := endTransactionTime
@@ -1069,20 +1057,15 @@ func (s *TransactionService) GetAccountsTotalIncomeAndExpense(c *core.Context, u
for maxTransactionTime > 0 {
var transactions []*models.Transaction
finalConditionParams := make([]any, 0, 6)
finalConditionParams = append(finalConditionParams, conditionParams...)
finalConditionParams = append(finalConditionParams, minTransactionTime)
finalConditionParams = append(finalConditionParams, maxTransactionTime)
err := s.UserDataDB(uid).NewSession(c).Select("type, account_id, transaction_time, timezone_utc_offset, amount").Where(condition, finalConditionParams...).Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions)
err := s.UserDataDB(uid).NewSession(c).Select("uid, type, account_id, transaction_time, timezone_utc_offset, amount").Where("uid=? AND deleted=? AND (type=? OR type=?) AND transaction_time>=? AND transaction_time<=?", uid, false, models.TRANSACTION_DB_TYPE_INCOME, models.TRANSACTION_DB_TYPE_EXPENSE, minTransactionTime, maxTransactionTime).Limit(pageCount, 0).OrderBy("transaction_time desc").Find(&transactions)
if err != nil {
return nil, nil, err
return nil, err
}
allTransactions = append(allTransactions, transactions...)
if len(transactions) < pageCountForLoadTransactionAmounts {
if len(transactions) < pageCount {
maxTransactionTime = 0
break
}
@@ -1090,269 +1073,74 @@ func (s *TransactionService) GetAccountsTotalIncomeAndExpense(c *core.Context, u
maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1
}
incomeAmounts := make(map[int64]int64)
expenseAmounts := make(map[int64]int64)
totalAmounts := make(map[string]models.TransactionAccountsAmount)
for i := 0; i < len(allTransactions); i++ {
transaction := allTransactions[i]
timeZone := clientLocation
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
yearMonth := utils.FormatUnixTimeToYearMonth(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
if useTransactionTimezone {
timeZone = time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
}
localDateTime := utils.FormatUnixTimeToNumericLocalDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), timeZone)
if localDateTime < startLocalDateTime || localDateTime > endLocalDateTime {
continue
}
var amountsMap map[int64]int64
if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME {
amountsMap = incomeAmounts
} else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
amountsMap = expenseAmounts
}
totalAmounts, exists := amountsMap[transaction.AccountId]
monthAccountsAmounts, exists := totalAmounts[yearMonth]
if !exists {
totalAmounts = 0
monthAccountsAmounts = make(models.TransactionAccountsAmount)
totalAmounts[yearMonth] = monthAccountsAmounts
}
totalAmounts += transaction.Amount
amountsMap[transaction.AccountId] = totalAmounts
monthAccountAmount, exists := monthAccountsAmounts[transaction.AccountId]
if !exists {
monthAccountAmount = &models.TransactionAccountAmount{
AccountId: transaction.AccountId,
TotalIncomeAmount: 0,
TotalExpenseAmount: 0,
}
monthAccountsAmounts[transaction.AccountId] = monthAccountAmount
}
if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME {
monthAccountAmount.TotalIncomeAmount += transaction.Amount
} else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
monthAccountAmount.TotalExpenseAmount += transaction.Amount
}
}
return incomeAmounts, expenseAmounts, nil
return totalAmounts, nil
}
// GetAccountsAndCategoriesTotalIncomeAndExpense returns the every accounts and categories total income and expense amount by specific date range
func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64, utcOffset int16, useTransactionTimezone bool) ([]*models.Transaction, error) {
func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c *core.Context, uid int64, startUnixTime int64, endUnixTime int64) ([]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
clientLocation := time.FixedZone("Client Timezone", int(utcOffset)*60)
var startLocalDateTime, endLocalDateTime, startTransactionTime, endTransactionTime int64
condition := "uid=? AND deleted=? AND (type=? OR type=?)"
conditionParams := make([]any, 0, 8)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
if startUnixTime > 0 {
startLocalDateTime = utils.FormatUnixTimeToNumericLocalDateTime(startUnixTime, clientLocation)
startUnixTime = utils.GetMinUnixTimeWithSameLocalDateTime(startUnixTime, utcOffset)
startTransactionTime = utils.GetMinTransactionTimeFromUnixTime(startUnixTime)
condition = condition + " AND transaction_time>=?"
conditionParams = append(conditionParams, utils.GetMinTransactionTimeFromUnixTime(startUnixTime))
}
if endUnixTime > 0 {
endLocalDateTime = utils.FormatUnixTimeToNumericLocalDateTime(endUnixTime, clientLocation)
endUnixTime = utils.GetMaxUnixTimeWithSameLocalDateTime(endUnixTime, utcOffset)
endTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(endUnixTime)
condition = condition + " AND transaction_time<=?"
conditionParams = append(conditionParams, utils.GetMaxTransactionTimeFromUnixTime(endUnixTime))
}
condition := "uid=? AND deleted=? AND (type=? OR type=?)"
conditionParams := make([]any, 0, 4)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
var transactionTotalAmounts []*models.Transaction
err := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, SUM(amount) as amount").Where(condition, conditionParams...).GroupBy("category_id, account_id").Find(&transactionTotalAmounts)
minTransactionTime := startTransactionTime
maxTransactionTime := endTransactionTime
var allTransactions []*models.Transaction
for maxTransactionTime >= 0 {
var transactions []*models.Transaction
finalCondition := condition
finalConditionParams := make([]any, 0, 6)
finalConditionParams = append(finalConditionParams, conditionParams...)
if minTransactionTime > 0 {
finalCondition = finalCondition + " AND transaction_time>=?"
finalConditionParams = append(finalConditionParams, minTransactionTime)
}
if maxTransactionTime > 0 {
finalCondition = finalCondition + " AND transaction_time<=?"
finalConditionParams = append(finalConditionParams, maxTransactionTime)
}
err := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...).Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions)
if err != nil {
return nil, err
}
allTransactions = append(allTransactions, transactions...)
if len(transactions) < pageCountForLoadTransactionAmounts {
maxTransactionTime = -1
break
}
maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1
}
transactionTotalAmountsMap := make(map[string]*models.Transaction)
for i := 0; i < len(allTransactions); i++ {
transaction := allTransactions[i]
timeZone := clientLocation
if useTransactionTimezone {
timeZone = time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
}
localDateTime := utils.FormatUnixTimeToNumericLocalDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), timeZone)
if (startLocalDateTime > 0 && localDateTime < startLocalDateTime) || (endLocalDateTime > 0 && localDateTime > endLocalDateTime) {
continue
}
groupKey := fmt.Sprintf("%d_%d", transaction.CategoryId, transaction.AccountId)
totalAmounts, exists := transactionTotalAmountsMap[groupKey]
if !exists {
totalAmounts = &models.Transaction{
CategoryId: transaction.CategoryId,
AccountId: transaction.AccountId,
Amount: 0,
}
transactionTotalAmountsMap[groupKey] = totalAmounts
}
totalAmounts.Amount += transaction.Amount
}
transactionTotalAmounts := make([]*models.Transaction, 0, len(transactionTotalAmountsMap))
for _, totalAmounts := range transactionTotalAmountsMap {
transactionTotalAmounts = append(transactionTotalAmounts, totalAmounts)
if err != nil {
return nil, err
}
return transactionTotalAmounts, nil
}
// GetAccountsAndCategoriesMonthlyIncomeAndExpense returns the every accounts monthly income and expense amount by specific date range
func (s *TransactionService) GetAccountsAndCategoriesMonthlyIncomeAndExpense(c *core.Context, uid int64, startYear int32, startMonth int32, endYear int32, endMonth int32, utcOffset int16, useTransactionTimezone bool) (map[int32][]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
clientLocation := time.FixedZone("Client Timezone", int(utcOffset)*60)
var startTransactionTime, endTransactionTime int64
var err error
if startYear > 0 && startMonth > 0 {
startTransactionTime, _, err = utils.GetTransactionTimeRangeByYearMonth(startYear, startMonth)
if err != nil {
return nil, errs.ErrSystemError
}
}
if endYear > 0 && endMonth > 0 {
_, endTransactionTime, err = utils.GetTransactionTimeRangeByYearMonth(endYear, endMonth)
if err != nil {
return nil, errs.ErrSystemError
}
}
condition := "uid=? AND deleted=? AND (type=? OR type=?)"
conditionParams := make([]any, 0, 4)
conditionParams = append(conditionParams, uid)
conditionParams = append(conditionParams, false)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
minTransactionTime := startTransactionTime
maxTransactionTime := endTransactionTime
var allTransactions []*models.Transaction
for maxTransactionTime >= 0 {
var transactions []*models.Transaction
finalCondition := condition
finalConditionParams := make([]any, 0, 6)
finalConditionParams = append(finalConditionParams, conditionParams...)
if minTransactionTime > 0 {
finalCondition = finalCondition + " AND transaction_time>=?"
finalConditionParams = append(finalConditionParams, minTransactionTime)
}
if maxTransactionTime > 0 {
finalCondition = finalCondition + " AND transaction_time<=?"
finalConditionParams = append(finalConditionParams, maxTransactionTime)
}
err := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...).Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions)
if err != nil {
return nil, err
}
allTransactions = append(allTransactions, transactions...)
if len(transactions) < pageCountForLoadTransactionAmounts {
maxTransactionTime = -1
break
}
maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1
}
startYearMonth := startYear*100 + startMonth
endYearMonth := endYear*100 + endMonth
transactionsMonthlyAmountsMap := make(map[string]*models.Transaction)
transactionsMonthlyAmounts := make(map[int32][]*models.Transaction)
for i := 0; i < len(allTransactions); i++ {
transaction := allTransactions[i]
timeZone := clientLocation
if useTransactionTimezone {
timeZone = time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
}
yearMonth := utils.FormatUnixTimeToNumericYearMonth(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), timeZone)
if (startYearMonth > 0 && yearMonth < startYearMonth) || (endYearMonth > 0 && yearMonth > endYearMonth) {
continue
}
groupKey := fmt.Sprintf("%d_%d_%d", yearMonth, transaction.CategoryId, transaction.AccountId)
transactionAmounts, exists := transactionsMonthlyAmountsMap[groupKey]
if !exists {
transactionAmounts = &models.Transaction{
CategoryId: transaction.CategoryId,
AccountId: transaction.AccountId,
}
transactionsMonthlyAmountsMap[groupKey] = transactionAmounts
}
transactionAmounts.Amount += transaction.Amount
}
for groupKey, transaction := range transactionsMonthlyAmountsMap {
groupKeyParts := strings.Split(groupKey, "_")
yearMonth, _ := utils.StringToInt32(groupKeyParts[0])
monthlyAmounts, exists := transactionsMonthlyAmounts[yearMonth]
if !exists {
monthlyAmounts = make([]*models.Transaction, 0, 0)
}
monthlyAmounts = append(monthlyAmounts, transaction)
transactionsMonthlyAmounts[yearMonth] = monthlyAmounts
}
return transactionsMonthlyAmounts, nil
}
// GetTransactionMapByList returns a transaction map by a list
func (s *TransactionService) GetTransactionMapByList(transactions []*models.Transaction) map[int64]*models.Transaction {
transactionMap := make(map[int64]*models.Transaction)
@@ -1365,7 +1153,7 @@ func (s *TransactionService) GetTransactionMapByList(transactions []*models.Tran
return transactionMap
}
func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, tagIds []int64, amountFilter string, keyword string, noDuplicated bool) (string, []any) {
func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionDbType, categoryIds []int64, accountIds []int64, keyword string, noDuplicated bool) (string, []any) {
condition := "uid=? AND deleted=?"
conditionParams := make([]any, 0, 16)
conditionParams = append(conditionParams, uid)
@@ -1381,18 +1169,6 @@ func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransact
conditionParams = append(conditionParams, minTransactionTime)
}
var accountIdsCondition strings.Builder
accountIdConditionParams := make([]any, 0, len(accountIds))
for i := 0; i < len(accountIds); i++ {
if i > 0 {
accountIdsCondition.WriteString(",")
}
accountIdsCondition.WriteString("?")
accountIdConditionParams = append(accountIdConditionParams, accountIds[i])
}
if models.TRANSACTION_DB_TYPE_MODIFY_BALANCE <= transactionType && transactionType <= models.TRANSACTION_DB_TYPE_EXPENSE {
condition = condition + " AND type=?"
conditionParams = append(conditionParams, transactionType)
@@ -1400,35 +1176,18 @@ func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransact
if len(accountIds) == 0 {
condition = condition + " AND type=?"
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT)
} else if len(accountIds) == 1 {
} else {
condition = condition + " AND (type=? OR type=?)"
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_IN)
} else { // len(accountsIds) > 1
condition = condition + " AND (type=? OR (type=? AND related_account_id NOT IN (" + accountIdsCondition.String() + ")))"
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_IN)
conditionParams = append(conditionParams, accountIdConditionParams...)
}
} else {
if noDuplicated {
if len(accountIds) == 0 {
condition = condition + " AND (type=? OR type=? OR type=? OR type=?)"
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_MODIFY_BALANCE)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT)
} else if len(accountIds) == 1 {
// Do Nothing
} else { // len(accountsIds) > 1
condition = condition + " AND (type=? OR type=? OR type=? OR type=? OR (type=? AND related_account_id NOT IN (" + accountIdsCondition.String() + ")))"
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_MODIFY_BALANCE)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_IN)
conditionParams = append(conditionParams, accountIdConditionParams...)
}
if noDuplicated && len(accountIds) == 0 {
condition = condition + " AND (type=? OR type=? OR type=? OR type=?)"
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_MODIFY_BALANCE)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_TRANSFER_OUT)
}
}
@@ -1444,73 +1203,22 @@ func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransact
conditionParams = append(conditionParams, categoryIds[i])
}
if conditions.Len() > 1 {
condition = condition + " AND category_id IN (" + conditions.String() + ")"
} else {
condition = condition + " AND category_id = " + conditions.String()
}
condition = condition + " AND category_id IN (" + conditions.String() + ")"
}
if len(accountIds) > 0 {
if accountIdsCondition.Len() > 1 {
condition = condition + " AND account_id IN (" + accountIdsCondition.String() + ")"
} else {
condition = condition + " AND account_id = " + accountIdsCondition.String()
var conditions strings.Builder
for i := 0; i < len(accountIds); i++ {
if i > 0 {
conditions.WriteString(",")
}
conditions.WriteString("?")
conditionParams = append(conditionParams, accountIds[i])
}
conditionParams = append(conditionParams, accountIdConditionParams...)
}
if amountFilter != "" {
amountFilterItems := strings.Split(amountFilter, ":")
if len(amountFilterItems) == 2 && amountFilterItems[0] == "gt" {
value, err := utils.StringToInt64(amountFilterItems[1])
if err == nil {
condition = condition + " AND amount > ?"
conditionParams = append(conditionParams, value)
}
} else if len(amountFilterItems) == 2 && amountFilterItems[0] == "lt" {
value, err := utils.StringToInt64(amountFilterItems[1])
if err == nil {
condition = condition + " AND amount < ?"
conditionParams = append(conditionParams, value)
}
} else if len(amountFilterItems) == 2 && amountFilterItems[0] == "eq" {
value, err := utils.StringToInt64(amountFilterItems[1])
if err == nil {
condition = condition + " AND amount = ?"
conditionParams = append(conditionParams, value)
}
} else if len(amountFilterItems) == 2 && amountFilterItems[0] == "ne" {
value, err := utils.StringToInt64(amountFilterItems[1])
if err == nil {
condition = condition + " AND amount <> ?"
conditionParams = append(conditionParams, value)
}
} else if len(amountFilterItems) == 3 && amountFilterItems[0] == "bt" {
value1, err := utils.StringToInt64(amountFilterItems[1])
value2, err := utils.StringToInt64(amountFilterItems[2])
if err == nil {
condition = condition + " AND amount >= ? AND amount <= ?"
conditionParams = append(conditionParams, value1)
conditionParams = append(conditionParams, value2)
}
} else if len(amountFilterItems) == 3 && amountFilterItems[0] == "nb" {
value1, err := utils.StringToInt64(amountFilterItems[1])
value2, err := utils.StringToInt64(amountFilterItems[2])
if err == nil {
condition = condition + " AND (amount < ? OR amount > ?)"
conditionParams = append(conditionParams, value1)
conditionParams = append(conditionParams, value2)
}
}
condition = condition + " AND account_id IN (" + conditions.String() + ")"
}
if keyword != "" {
@@ -1521,40 +1229,6 @@ func (s *TransactionService) getTransactionQueryCondition(uid int64, maxTransact
return condition, conditionParams
}
func (s *TransactionService) getTransactionQueryByTagIdsCondition(uid int64, maxTransactionTime int64, minTransactionTime int64, tagIds []int64) *builder.Builder {
if len(tagIds) > 0 {
condition := builder.And(builder.Eq{"uid": uid}, builder.Eq{"deleted": false})
if maxTransactionTime > 0 {
condition = condition.And(builder.Lte{"transaction_time": maxTransactionTime})
}
if minTransactionTime > 0 {
condition = condition.And(builder.Gte{"transaction_time": minTransactionTime})
}
condition = condition.And(builder.In("tag_id", tagIds))
return builder.Select("transaction_id").From("transaction_tag_index").Where(condition)
}
return nil
}
func (s *TransactionService) getTransactionQueryByAllTagIdsCondition(uid int64, maxTransactionTime int64, minTransactionTime int64) *builder.Builder {
condition := builder.And(builder.Eq{"uid": uid}, builder.Eq{"deleted": false})
if maxTransactionTime > 0 {
condition = condition.And(builder.Lte{"transaction_time": maxTransactionTime})
}
if minTransactionTime > 0 {
condition = condition.And(builder.Gte{"transaction_time": minTransactionTime})
}
return builder.Select("transaction_id").From("transaction_tag_index").Where(condition)
}
func (s *TransactionService) isAccountIdValid(transaction *models.Transaction) error {
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
if transaction.RelatedAccountId != 0 && transaction.RelatedAccountId != transaction.AccountId {
@@ -1592,7 +1266,6 @@ func (s *TransactionService) getAccountModels(sess *xorm.Session, transaction *m
return nil, nil, errs.ErrSourceAccountNotFound
}
// check whether the related account is valid
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
if transaction.RelatedAccountId != 0 && transaction.RelatedAccountId != transaction.AccountId {
return nil, nil, errs.ErrAccountIdInvalid
@@ -1619,54 +1292,6 @@ func (s *TransactionService) getAccountModels(sess *xorm.Session, transaction *m
}
}
// check whether the parent accounts are valid
if sourceAccount.ParentAccountId > 0 && destinationAccount != nil && sourceAccount.ParentAccountId != destinationAccount.ParentAccountId && destinationAccount.ParentAccountId > 0 {
var accounts []*models.Account
err := sess.Where("uid=? AND deleted=? and (account_id=? or account_id=?)", transaction.Uid, false, sourceAccount.ParentAccountId, destinationAccount.ParentAccountId).Find(&accounts)
if err != nil {
return nil, nil, err
}
if len(accounts) < 2 {
return nil, nil, errs.ErrAccountNotFound
}
for i := 0; i < len(accounts); i++ {
account := accounts[i]
if account.Hidden {
return nil, nil, errs.ErrCannotUseHiddenAccount
}
}
} else if sourceAccount.ParentAccountId > 0 && (destinationAccount == nil || sourceAccount.ParentAccountId == destinationAccount.ParentAccountId || destinationAccount.ParentAccountId == 0) {
sourceParentAccount := &models.Account{}
has, err = sess.ID(sourceAccount.ParentAccountId).Where("uid=? AND deleted=?", transaction.Uid, false).Get(sourceParentAccount)
if err != nil {
return nil, nil, err
} else if !has {
return nil, nil, errs.ErrSourceAccountNotFound
}
if sourceParentAccount.Hidden {
return nil, nil, errs.ErrCannotUseHiddenAccount
}
} else if sourceAccount.ParentAccountId == 0 && destinationAccount != nil && destinationAccount.ParentAccountId > 0 {
destinationParentAccount := &models.Account{}
has, err = sess.ID(destinationAccount.ParentAccountId).Where("uid=? AND deleted=?", transaction.Uid, false).Get(destinationParentAccount)
if err != nil {
return nil, nil, err
} else if !has {
return nil, nil, errs.ErrDestinationAccountNotFound
}
if destinationParentAccount.Hidden {
return nil, nil, errs.ErrCannotUseHiddenAccount
}
}
return sourceAccount, destinationAccount, nil
}
@@ -1735,10 +1360,6 @@ func (s *TransactionService) isCategoryValid(sess *xorm.Session, transaction *mo
return errs.ErrTransactionCategoryNotFound
}
if category.Hidden {
return errs.ErrCannotUseHiddenTransactionCategory
}
if category.ParentCategoryId < 1 {
return errs.ErrCannotUsePrimaryCategoryForTransaction
}
@@ -1748,26 +1369,13 @@ func (s *TransactionService) isCategoryValid(sess *xorm.Session, transaction *mo
((transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN) && category.Type != models.CATEGORY_TYPE_TRANSFER) {
return errs.ErrTransactionCategoryTypeInvalid
}
parentCategory := &models.TransactionCategory{}
has, err = sess.ID(category.ParentCategoryId).Where("uid=? AND deleted=?", transaction.Uid, false).Get(parentCategory)
if err != nil {
return err
} else if !has {
return errs.ErrTransactionCategoryNotFound
}
if parentCategory.Hidden {
return errs.ErrCannotUseHiddenTransactionCategory
}
}
return nil
}
func (s *TransactionService) isTagsValid(sess *xorm.Session, transaction *models.Transaction, transactionTagIndexes []*models.TransactionTagIndex, tagIds []int64) error {
if len(transactionTagIndexes) > 0 {
func (s *TransactionService) isTagsValid(sess *xorm.Session, transaction *models.Transaction, transactionTagIndexs []*models.TransactionTagIndex, tagIds []int64) error {
if len(transactionTagIndexs) > 0 {
var tags []*models.TransactionTag
err := sess.Where("uid=? AND deleted=?", transaction.Uid, false).In("tag_id", tagIds).Find(&tags)
@@ -1778,15 +1386,11 @@ func (s *TransactionService) isTagsValid(sess *xorm.Session, transaction *models
tagMap := make(map[int64]*models.TransactionTag)
for i := 0; i < len(tags); i++ {
if tags[i].Hidden {
return errs.ErrCannotUseHiddenTransactionTag
}
tagMap[tags[i].TagId] = tags[i]
}
for i := 0; i < len(transactionTagIndexes); i++ {
if _, exists := tagMap[transactionTagIndexes[i].TagId]; !exists {
for i := 0; i < len(transactionTagIndexs); i++ {
if _, exists := tagMap[transactionTagIndexs[i].TagId]; !exists {
return errs.ErrTransactionTagNotFound
}
}
-43
View File
@@ -248,30 +248,6 @@ func (s *UserService) UpdateUser(c *core.Context, user *models.User, modifyUserL
updateCols = append(updateCols, "short_time_format")
}
if core.DECIMAL_SEPARATOR_DEFAULT <= user.DecimalSeparator && user.DecimalSeparator <= core.DECIMAL_SEPARATOR_SPACE {
updateCols = append(updateCols, "decimal_separator")
}
if core.DIGIT_GROUPING_SYMBOL_DEFAULT <= user.DigitGroupingSymbol && user.DigitGroupingSymbol <= core.DIGIT_GROUPING_SYMBOL_APOSTROPHE {
updateCols = append(updateCols, "digit_grouping_symbol")
}
if core.DIGIT_GROUPING_TYPE_DEFAULT <= user.DigitGrouping && user.DigitGrouping <= core.DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR {
updateCols = append(updateCols, "digit_grouping")
}
if models.CURRENCY_DISPLAY_TYPE_DEFAULT <= user.CurrencyDisplayType && user.CurrencyDisplayType <= models.CURRENCY_DISPLAY_TYPE_NAME_AFTER_AMOUNT {
updateCols = append(updateCols, "currency_display_type")
}
if models.AMOUNT_COLOR_TYPE_DEFAULT <= user.ExpenseAmountColor && user.ExpenseAmountColor <= models.AMOUNT_COLOR_TYPE_BLACK_OR_WHITE {
updateCols = append(updateCols, "expense_amount_color")
}
if models.AMOUNT_COLOR_TYPE_DEFAULT <= user.IncomeAmountColor && user.IncomeAmountColor <= models.AMOUNT_COLOR_TYPE_BLACK_OR_WHITE {
updateCols = append(updateCols, "income_amount_color")
}
user.UpdatedUnixTime = now
updateCols = append(updateCols, "updated_unix_time")
@@ -294,25 +270,6 @@ func (s *UserService) UpdateUser(c *core.Context, user *models.User, modifyUserL
return keyProfileUpdated, emailSetToUnverified, nil
}
// UpdateUserAvatar updated the custom avatar type of specified user
func (s *UserService) UpdateUserAvatar(c *core.Context, uid int64, customAvatarType string) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
now := time.Now().Unix()
updateModel := &models.User{
CustomAvatarType: customAvatarType,
UpdatedUnixTime: now,
}
return s.UserDB().DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.ID(uid).Cols("custom_avatar_type", "updated_unix_time").Where("deleted=?", false).Update(updateModel)
return err
})
}
// UpdateUserLastLoginTime updates the last login time field
func (s *UserService) UpdateUserLastLoginTime(c *core.Context, uid int64) error {
if uid <= 0 {
+31 -321
View File
@@ -11,7 +11,6 @@ import (
"gopkg.in/ini.v1"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/locales"
)
const (
@@ -58,26 +57,14 @@ const (
Sqlite3DbType string = "sqlite3"
)
// Object Storage types
const (
LocalFileSystemObjectStorageType string = "local_filesystem"
MinIOStorageType string = "minio"
)
// Uuid generator types
const (
InternalUuidGeneratorType string = "internal"
)
// Duplicate checker types
const (
InMemoryDuplicateCheckerType string = "in_memory"
)
// User avatar provider types
const (
InternalAvatarProvider string = "internal"
GravatarProvider string = "gravatar"
GravatarProvider string = "gravatar"
)
// Map provider types
@@ -87,13 +74,10 @@ const (
OpenTopoMapProvider string = "opentopomap"
OPNVKarteMapProvider string = "opnvkarte"
CyclOSMMapProvider string = "cyclosm"
CartoDBMapProvider string = "cartodb"
TomTomMapProvider string = "tomtom"
TianDiTuProvider string = "tianditu"
GoogleMapProvider string = "googlemap"
TomTomMapProvider string = "tomtom"
BaiduMapProvider string = "baidumap"
AmapProvider string = "amap"
CustomProvider string = "custom"
)
// Amap security verification method
@@ -126,19 +110,14 @@ const (
defaultDatabaseMaxOpenConn uint16 = 0
defaultDatabaseConnMaxLifetime uint32 = 14400
defaultLogMode string = "console"
defaultLogFileMaxSize uint32 = 104857600 // 100 MB
defaultLogFileMaxDays uint32 = 7 // days
defaultInMemoryDuplicateCheckerCleanupInterval uint32 = 60 // 1 minutes
defaultDuplicateSubmissionsInterval uint32 = 300 // 5 minutes
defaultLogMode string = "console"
defaultLoglevel Level = LOGLEVEL_INFO
defaultSecretKey string = "ezbookkeeping"
defaultTokenExpiredTime uint32 = 2592000 // 30 days
defaultTokenMinRefreshInterval uint32 = 86400 // 1 day
defaultTemporaryTokenExpiredTime uint32 = 300 // 5 minutes
defaultEmailVerifyTokenExpiredTime uint32 = 3600 // 60 minutes
defaultPasswordResetTokenExpiredTime uint32 = 3600 // 60 minutes
defaultTokenExpiredTime uint32 = 604800 // 7 days
defaultTemporaryTokenExpiredTime uint32 = 300 // 5 minutes
defaultEmailVerifyTokenExpiredTime uint32 = 3600 // 60 minutes
defaultPasswordResetTokenExpiredTime uint32 = 3600 // 60 minutes
defaultExchangeRatesDataRequestTimeout uint32 = 10000 // 10 seconds
)
@@ -169,25 +148,6 @@ type SMTPConfig struct {
FromAddress string
}
// MinIOConfig represents the MinIO setting config
type MinIOConfig struct {
Endpoint string
Location string
AccessKeyID string
SecretAccessKey string
UseSSL bool
SkipTLSVerify bool
Bucket string
RootPath string
}
// NotificationConfig represents a notification setting config
type NotificationConfig struct {
Enabled bool
DefaultContent string
MultiLanguageContent map[string]string
}
// Config represents the global setting config
type Config struct {
// Global
@@ -227,38 +187,18 @@ type Config struct {
EnableConsoleLog bool
EnableFileLog bool
LogLevel Level
FileLogPath string
RequestFileLogPath string
QueryFileLogPath string
LogFileRotate bool
LogFileMaxSize uint32
LogFileMaxDays uint32
// Storage
StorageType string
LocalFileSystemPath string
MinIOConfig *MinIOConfig
LogLevel Level
FileLogPath string
// Uuid
UuidGeneratorType string
UuidServerId uint8
// Duplicate Checker
DuplicateCheckerType string
InMemoryDuplicateCheckerCleanupInterval uint32
InMemoryDuplicateCheckerCleanupIntervalDuration time.Duration
EnableDuplicateSubmissionsCheck bool
DuplicateSubmissionsInterval uint32
DuplicateSubmissionsIntervalDuration time.Duration
// Secret
SecretKeyNoSet bool
SecretKey string
EnableTwoFactor bool
TokenExpiredTime uint32
TokenExpiredTimeDuration time.Duration
TokenMinRefreshInterval uint32
TemporaryTokenExpiredTime uint32
TemporaryTokenExpiredTimeDuration time.Duration
EmailVerifyTokenExpiredTime uint32
@@ -278,33 +218,20 @@ type Config struct {
// Data
EnableDataExport bool
// Notification
AfterRegisterNotification NotificationConfig
AfterLoginNotification NotificationConfig
AfterOpenNotification NotificationConfig
// Map
MapProvider string
EnableMapDataFetchProxy bool
MapProxy string
TomTomMapAPIKey string
TianDiTuAPIKey string
GoogleMapAPIKey string
BaiduMapAK string
AmapApplicationKey string
AmapSecurityVerificationMethod string
AmapApplicationSecret string
AmapApiExternalProxyUrl string
CustomMapTileServerTileLayerUrl string
CustomMapTileServerAnnotationLayerUrl string
CustomMapTileServerMinZoomLevel uint8
CustomMapTileServerMaxZoomLevel uint8
CustomMapTileServerDefaultZoomLevel uint8
MapProvider string
TomTomMapAPIKey string
GoogleMapAPIKey string
BaiduMapAK string
AmapApplicationKey string
AmapSecurityVerificationMethod string
AmapApplicationSecret string
AmapApiExternalProxyUrl string
EnableMapDataFetchProxy bool
// Exchange Rates
ExchangeRatesDataSource string
ExchangeRatesRequestTimeout uint32
ExchangeRatesProxy string
ExchangeRatesSkipTLSVerify bool
}
@@ -355,24 +282,12 @@ func LoadConfiguration(configFilePath string) (*Config, error) {
return nil, err
}
err = loadStorageConfiguration(config, cfgFile, "storage")
if err != nil {
return nil, err
}
err = loadUuidConfiguration(config, cfgFile, "uuid")
if err != nil {
return nil, err
}
err = loadDuplicateCheckerConfiguration(config, cfgFile, "duplicate_checker")
if err != nil {
return nil, err
}
err = loadSecurityConfiguration(config, cfgFile, "security")
if err != nil {
@@ -391,12 +306,6 @@ func LoadConfiguration(configFilePath string) (*Config, error) {
return nil, err
}
err = loadNotificationConfiguration(config, cfgFile, "notification")
if err != nil {
return nil, err
}
err = loadMapConfiguration(config, cfgFile, "map")
if err != nil {
@@ -432,13 +341,10 @@ func GetDefaultConfigFilePath() (string, error) {
func loadGlobalConfiguration(config *Config, configFile *ini.File, sectionName string) error {
config.AppName = getConfigItemStringValue(configFile, sectionName, "app_name", defaultAppName)
config.Mode = MODE_PRODUCTION
if getConfigItemStringValue(configFile, sectionName, "mode") == "production" {
config.Mode = MODE_PRODUCTION
} else if getConfigItemStringValue(configFile, sectionName, "mode") == "development" {
if getConfigItemStringValue(configFile, sectionName, "mode") == "development" {
config.Mode = MODE_DEVELOPMENT
} else {
return errs.ErrInvalidServerMode
}
return nil
@@ -492,13 +398,6 @@ func loadDatabaseConfiguration(config *Config, configFile *ini.File, sectionName
dbConfig := &DatabaseConfig{}
dbConfig.DatabaseType = getConfigItemStringValue(configFile, sectionName, "type", MySqlDbType)
if dbConfig.DatabaseType != MySqlDbType &&
dbConfig.DatabaseType != PostgresDbType &&
dbConfig.DatabaseType != Sqlite3DbType {
return errs.ErrDatabaseTypeInvalid
}
dbConfig.DatabaseHost = getConfigItemStringValue(configFile, sectionName, "host", defaultDatabaseHost)
dbConfig.DatabaseName = getConfigItemStringValue(configFile, sectionName, "name", defaultDatabaseName)
dbConfig.DatabaseUser = getConfigItemStringValue(configFile, sectionName, "user")
@@ -556,83 +455,17 @@ func loadLogConfiguration(config *Config, configFile *ini.File, sectionName stri
}
}
var err error
config.LogLevel, err = getLogLevel(getConfigItemStringValue(configFile, sectionName, "level"))
if err != nil {
return err
}
if config.LogLevel != LOGLEVEL_DEBUG &&
config.LogLevel != LOGLEVEL_INFO &&
config.LogLevel != LOGLEVEL_WARN &&
config.LogLevel != LOGLEVEL_ERROR {
return errs.ErrInvalidLogLevel
}
config.LogLevel = getLogLevel(getConfigItemStringValue(configFile, sectionName, "level"), defaultLoglevel)
if config.EnableFileLog {
fileLogPath := getConfigItemStringValue(configFile, sectionName, "log_path")
finalFileLogPath, _ := getFinalPath(config.WorkingPath, fileLogPath)
config.FileLogPath = finalFileLogPath
requestFileLogPath := getConfigItemStringValue(configFile, sectionName, "request_log_path")
if requestFileLogPath != "" {
finalRequestFileLogPath, _ := getFinalPath(config.WorkingPath, requestFileLogPath)
config.RequestFileLogPath = finalRequestFileLogPath
} else {
config.RequestFileLogPath = ""
}
queryFileLogPath := getConfigItemStringValue(configFile, sectionName, "query_log_path")
if queryFileLogPath != "" {
finalQueryFileLogPath, _ := getFinalPath(config.WorkingPath, queryFileLogPath)
config.QueryFileLogPath = finalQueryFileLogPath
} else {
config.QueryFileLogPath = ""
}
config.LogFileRotate = getConfigItemBoolValue(configFile, sectionName, "log_file_rotate", false)
config.LogFileMaxSize = getConfigItemUint32Value(configFile, sectionName, "log_file_max_size", defaultLogFileMaxSize)
config.LogFileMaxDays = getConfigItemUint32Value(configFile, sectionName, "log_file_max_days", defaultLogFileMaxDays)
}
return nil
}
func loadStorageConfiguration(config *Config, configFile *ini.File, sectionName string) error {
if getConfigItemStringValue(configFile, sectionName, "type") == LocalFileSystemObjectStorageType {
config.StorageType = LocalFileSystemObjectStorageType
} else if getConfigItemStringValue(configFile, sectionName, "type") == MinIOStorageType {
config.StorageType = MinIOStorageType
} else {
return errs.ErrInvalidStorageType
}
localFileSystemRootPath := getConfigItemStringValue(configFile, sectionName, "local_filesystem_path")
finalLocalFileSystemRootPath, err := getFinalPath(config.WorkingPath, localFileSystemRootPath)
config.LocalFileSystemPath = finalLocalFileSystemRootPath
if config.StorageType == LocalFileSystemObjectStorageType && err != nil {
return errs.ErrInvalidLocalFileSystemStoragePath
}
minIOConfig := &MinIOConfig{}
minIOConfig.Endpoint = getConfigItemStringValue(configFile, sectionName, "minio_endpoint")
minIOConfig.Location = getConfigItemStringValue(configFile, sectionName, "minio_location")
minIOConfig.AccessKeyID = getConfigItemStringValue(configFile, sectionName, "minio_access_key_id")
minIOConfig.SecretAccessKey = getConfigItemStringValue(configFile, sectionName, "minio_secret_access_key")
minIOConfig.UseSSL = getConfigItemBoolValue(configFile, sectionName, "minio_use_ssl", false)
minIOConfig.SkipTLSVerify = getConfigItemBoolValue(configFile, sectionName, "minio_skip_tls_verify", false)
minIOConfig.Bucket = getConfigItemStringValue(configFile, sectionName, "minio_bucket")
minIOConfig.RootPath = getConfigItemStringValue(configFile, sectionName, "minio_root_path")
config.MinIOConfig = minIOConfig
return nil
}
func loadUuidConfiguration(config *Config, configFile *ini.File, sectionName string) error {
if getConfigItemStringValue(configFile, sectionName, "generator_type") == InternalUuidGeneratorType {
config.UuidGeneratorType = InternalUuidGeneratorType
@@ -645,76 +478,20 @@ func loadUuidConfiguration(config *Config, configFile *ini.File, sectionName str
return nil
}
func loadDuplicateCheckerConfiguration(config *Config, configFile *ini.File, sectionName string) error {
if getConfigItemStringValue(configFile, sectionName, "checker_type") == InMemoryDuplicateCheckerType {
config.DuplicateCheckerType = InMemoryDuplicateCheckerType
} else {
return errs.ErrInvalidDuplicateCheckerType
}
config.InMemoryDuplicateCheckerCleanupInterval = getConfigItemUint32Value(configFile, sectionName, "cleanup_interval", defaultInMemoryDuplicateCheckerCleanupInterval)
if config.InMemoryDuplicateCheckerCleanupInterval < 1 {
return errs.ErrInvalidInMemoryDuplicateCheckerCleanupInterval
}
config.InMemoryDuplicateCheckerCleanupIntervalDuration = time.Duration(config.InMemoryDuplicateCheckerCleanupInterval) * time.Second
duplicateSubmissionsInterval := getConfigItemUint32Value(configFile, sectionName, "duplicate_submissions_interval", defaultDuplicateSubmissionsInterval)
config.EnableDuplicateSubmissionsCheck = duplicateSubmissionsInterval > 0
if duplicateSubmissionsInterval < 1 {
duplicateSubmissionsInterval = defaultDuplicateSubmissionsInterval
}
config.DuplicateSubmissionsInterval = duplicateSubmissionsInterval
config.DuplicateSubmissionsIntervalDuration = time.Duration(config.DuplicateSubmissionsInterval) * time.Second
return nil
}
func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName string) error {
config.SecretKeyNoSet = !getConfigItemIsSet(configFile, sectionName, "secret_key")
config.SecretKey = getConfigItemStringValue(configFile, sectionName, "secret_key", defaultSecretKey)
config.EnableTwoFactor = getConfigItemBoolValue(configFile, sectionName, "enable_two_factor", true)
config.TokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "token_expired_time", defaultTokenExpiredTime)
if config.TokenExpiredTime < 60 {
return errs.ErrInvalidTokenExpiredTime
}
config.TokenExpiredTimeDuration = time.Duration(config.TokenExpiredTime) * time.Second
config.TokenMinRefreshInterval = getConfigItemUint32Value(configFile, sectionName, "token_min_refresh_interval", defaultTokenMinRefreshInterval)
if config.TokenMinRefreshInterval >= config.TokenExpiredTime {
return errs.ErrInvalidTokenMinRefreshInterval
}
config.TemporaryTokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "temporary_token_expired_time", defaultTemporaryTokenExpiredTime)
if config.TemporaryTokenExpiredTime < 60 {
return errs.ErrInvalidTemporaryTokenExpiredTime
}
config.TemporaryTokenExpiredTimeDuration = time.Duration(config.TemporaryTokenExpiredTime) * time.Second
config.EmailVerifyTokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "email_verify_token_expired_time", defaultEmailVerifyTokenExpiredTime)
if config.EmailVerifyTokenExpiredTime < 60 {
return errs.ErrInvalidEmailVerifyTokenExpiredTime
}
config.EmailVerifyTokenExpiredTimeDuration = time.Duration(config.EmailVerifyTokenExpiredTime) * time.Second
config.PasswordResetTokenExpiredTime = getConfigItemUint32Value(configFile, sectionName, "password_reset_token_expired_time", defaultPasswordResetTokenExpiredTime)
if config.PasswordResetTokenExpiredTime < 60 {
return errs.ErrInvalidPasswordResetTokenExpiredTime
}
config.PasswordResetTokenExpiredTimeDuration = time.Duration(config.PasswordResetTokenExpiredTime) * time.Second
config.EnableRequestIdHeader = getConfigItemBoolValue(configFile, sectionName, "request_id_header", true)
@@ -729,14 +506,10 @@ func loadUserConfiguration(config *Config, configFile *ini.File, sectionName str
config.EnableUserForgetPassword = getConfigItemBoolValue(configFile, sectionName, "enable_forget_password", false)
config.ForgetPasswordRequireVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "forget_password_require_email_verify", false)
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == InternalAvatarProvider {
config.AvatarProvider = InternalAvatarProvider
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == "" {
config.AvatarProvider = ""
} else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == GravatarProvider {
config.AvatarProvider = GravatarProvider
} else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == "" {
config.AvatarProvider = ""
} else {
return errs.ErrInvalidAvatarProvider
}
return nil
@@ -748,14 +521,6 @@ func loadDataConfiguration(config *Config, configFile *ini.File, sectionName str
return nil
}
func loadNotificationConfiguration(config *Config, configFile *ini.File, sectionName string) error {
config.AfterRegisterNotification = getNotificationConfiguration(configFile, sectionName, "enable_notification_after_register", "after_register_notification_content")
config.AfterLoginNotification = getNotificationConfiguration(configFile, sectionName, "enable_notification_after_login", "after_login_notification_content")
config.AfterOpenNotification = getNotificationConfiguration(configFile, sectionName, "enable_notification_after_open", "after_open_notification_content")
return nil
}
func loadMapConfiguration(config *Config, configFile *ini.File, sectionName string) error {
mapProvider := getConfigItemStringValue(configFile, sectionName, "map_provider")
@@ -771,28 +536,20 @@ func loadMapConfiguration(config *Config, configFile *ini.File, sectionName stri
config.MapProvider = OPNVKarteMapProvider
} else if mapProvider == CyclOSMMapProvider {
config.MapProvider = CyclOSMMapProvider
} else if mapProvider == CartoDBMapProvider {
config.MapProvider = CartoDBMapProvider
} else if mapProvider == TomTomMapProvider {
config.MapProvider = TomTomMapProvider
} else if mapProvider == TianDiTuProvider {
config.MapProvider = TianDiTuProvider
} else if mapProvider == GoogleMapProvider {
config.MapProvider = GoogleMapProvider
} else if mapProvider == TomTomMapProvider {
config.MapProvider = TomTomMapProvider
} else if mapProvider == BaiduMapProvider {
config.MapProvider = BaiduMapProvider
} else if mapProvider == AmapProvider {
config.MapProvider = AmapProvider
} else if mapProvider == CustomProvider {
config.MapProvider = CustomProvider
} else {
return errs.ErrInvalidMapProvider
}
config.EnableMapDataFetchProxy = getConfigItemBoolValue(configFile, sectionName, "map_data_fetch_proxy", false)
config.MapProxy = getConfigItemStringValue(configFile, sectionName, "proxy", "system")
config.TomTomMapAPIKey = getConfigItemStringValue(configFile, sectionName, "tomtom_map_api_key")
config.TianDiTuAPIKey = getConfigItemStringValue(configFile, sectionName, "tianditu_map_app_key")
config.GoogleMapAPIKey = getConfigItemStringValue(configFile, sectionName, "google_map_api_key")
config.BaiduMapAK = getConfigItemStringValue(configFile, sectionName, "baidu_map_ak")
config.AmapApplicationKey = getConfigItemStringValue(configFile, sectionName, "amap_application_key")
@@ -812,12 +569,6 @@ func loadMapConfiguration(config *Config, configFile *ini.File, sectionName stri
config.AmapApplicationSecret = getConfigItemStringValue(configFile, sectionName, "amap_application_secret")
config.AmapApiExternalProxyUrl = getConfigItemStringValue(configFile, sectionName, "amap_api_external_proxy_url")
config.CustomMapTileServerTileLayerUrl = getConfigItemStringValue(configFile, sectionName, "custom_map_tile_server_url")
config.CustomMapTileServerAnnotationLayerUrl = getConfigItemStringValue(configFile, sectionName, "custom_map_tile_server_annotation_url")
config.CustomMapTileServerMinZoomLevel = getConfigItemUint8Value(configFile, sectionName, "custom_map_tile_server_min_zoom_level", 1)
config.CustomMapTileServerMaxZoomLevel = getConfigItemUint8Value(configFile, sectionName, "custom_map_tile_server_max_zoom_level", 18)
config.CustomMapTileServerDefaultZoomLevel = getConfigItemUint8Value(configFile, sectionName, "custom_map_tile_server_default_zoom_level", 14)
return nil
}
func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectionName string) error {
@@ -839,7 +590,6 @@ func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectio
return errs.ErrInvalidExchangeRatesDataSource
}
config.ExchangeRatesProxy = getConfigItemStringValue(configFile, sectionName, "proxy", "system")
config.ExchangeRatesRequestTimeout = getConfigItemUint32Value(configFile, sectionName, "request_timeout", defaultExchangeRatesDataRequestTimeout)
config.ExchangeRatesSkipTLSVerify = getConfigItemBoolValue(configFile, sectionName, "skip_tls_verify", false)
@@ -876,44 +626,6 @@ func getFinalPath(workingPath, p string) (string, error) {
return p, err
}
func getNotificationConfiguration(configFile *ini.File, sectionName string, enableKey string, contentKey string) NotificationConfig {
config := NotificationConfig{
Enabled: getConfigItemBoolValue(configFile, sectionName, enableKey, false),
DefaultContent: getConfigItemStringValue(configFile, sectionName, contentKey, ""),
MultiLanguageContent: make(map[string]string),
}
for languageTag := range locales.AllLanguages {
multiLanguageContentKey := strings.ToLower(languageTag)
multiLanguageContentKey = strings.Replace(multiLanguageContentKey, "-", "_", -1)
multiLanguageContentKey = contentKey + "_" + multiLanguageContentKey
content := getConfigItemStringValue(configFile, sectionName, multiLanguageContentKey, "")
if content != "" {
config.MultiLanguageContent[languageTag] = content
}
}
return config
}
func getConfigItemIsSet(configFile *ini.File, sectionName string, itemName string) bool {
environmentKey := getEnvironmentKey(sectionName, itemName)
environmentValue := os.Getenv(environmentKey)
if len(environmentValue) > 0 {
return true
}
section := configFile.Section(sectionName)
if !section.HasKey(itemName) {
return false
}
return section.Key(itemName).String() != ""
}
func getConfigItemStringValue(configFile *ini.File, sectionName string, itemName string, defaultValue ...string) string {
environmentKey := getEnvironmentKey(sectionName, itemName)
environmentValue := os.Getenv(environmentKey)
@@ -1017,16 +729,14 @@ func getEnvironmentKey(sectionName string, itemName string) string {
return fmt.Sprintf("%s_%s_%s", ebkEnvNamePrefix, strings.ToUpper(sectionName), strings.ToUpper(itemName))
}
func getLogLevel(logLevelStr string) (Level, error) {
func getLogLevel(logLevelStr string, defaultLogLevel Level) Level {
if logLevelStr == "debug" {
return LOGLEVEL_DEBUG, nil
} else if logLevelStr == "info" {
return LOGLEVEL_INFO, nil
return LOGLEVEL_DEBUG
} else if logLevelStr == "warn" {
return LOGLEVEL_WARN, nil
return LOGLEVEL_WARN
} else if logLevelStr == "error" {
return LOGLEVEL_ERROR, nil
return LOGLEVEL_ERROR
}
return "", errs.ErrInvalidLogLevel
return defaultLogLevel
}
-57
View File
@@ -16,60 +16,3 @@ var (
func SetCurrentConfig(config *Config) {
Container.Current = config
}
// GetAfterRegisterNotificationContent returns the notification content displayed each time users register
func (c *ConfigContainer) GetAfterRegisterNotificationContent(userLanguage string, clientLanguage string) string {
language := userLanguage
if language == "" {
language = clientLanguage
}
if !c.Current.AfterRegisterNotification.Enabled {
return ""
}
if multiLanguageContent, exists := c.Current.AfterRegisterNotification.MultiLanguageContent[language]; exists {
return multiLanguageContent
}
return c.Current.AfterRegisterNotification.DefaultContent
}
// GetAfterLoginNotificationContent returns the notification content displayed each time users log in
func (c *ConfigContainer) GetAfterLoginNotificationContent(userLanguage string, clientLanguage string) string {
language := userLanguage
if language == "" {
language = clientLanguage
}
if !c.Current.AfterLoginNotification.Enabled {
return ""
}
if multiLanguageContent, exists := c.Current.AfterLoginNotification.MultiLanguageContent[language]; exists {
return multiLanguageContent
}
return c.Current.AfterLoginNotification.DefaultContent
}
// GetAfterOpenNotificationContent returns the notification content displayed each time users open the app
func (c *ConfigContainer) GetAfterOpenNotificationContent(userLanguage string, clientLanguage string) string {
language := userLanguage
if language == "" {
language = clientLanguage
}
if !c.Current.AfterOpenNotification.Enabled {
return ""
}
if multiLanguageContent, exists := c.Current.AfterOpenNotification.MultiLanguageContent[language]; exists {
return multiLanguageContent
}
return c.Current.AfterOpenNotification.DefaultContent
}
-68
View File
@@ -1,68 +0,0 @@
package storage
import (
"io"
"os"
"path/filepath"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// LocalFileSystemObjectStorage represents local file system object storage
type LocalFileSystemObjectStorage struct {
rootPath string
}
// NewLocalFileSystemObjectStorage returns a local file system object storage
func NewLocalFileSystemObjectStorage(config *settings.Config, pathPrefix string) (*LocalFileSystemObjectStorage, error) {
storage := &LocalFileSystemObjectStorage{
rootPath: filepath.Join(config.LocalFileSystemPath, pathPrefix),
}
if err := os.MkdirAll(storage.rootPath, os.ModePerm); err != nil {
return nil, err
}
return storage, nil
}
// Exists returns whether the file exists
func (s *LocalFileSystemObjectStorage) Exists(path string) (bool, error) {
return utils.IsExists(s.getFinalPath(path))
}
// Read returns the object instance according to specified the file path
func (s *LocalFileSystemObjectStorage) Read(path string) (ObjectInStorage, error) {
return os.Open(s.getFinalPath(path))
}
// Save returns whether save the object instance successfully
func (s *LocalFileSystemObjectStorage) Save(path string, object ObjectInStorage) error {
finalPath := s.getFinalPath(path)
if err := os.MkdirAll(filepath.Dir(finalPath), os.ModePerm); err != nil {
return err
}
targetFile, err := os.Create(finalPath)
if err != nil {
return err
}
defer targetFile.Close()
_, err = io.Copy(targetFile, object)
return err
}
// Delete returns whether delete the object according to specified the file path successfully
func (s *LocalFileSystemObjectStorage) Delete(path string) error {
return os.Remove(s.getFinalPath(path))
}
func (s *LocalFileSystemObjectStorage) getFinalPath(path string) string {
return filepath.Join(s.rootPath, path)
}
-108
View File
@@ -1,108 +0,0 @@
package storage
import (
"context"
"crypto/tls"
"net/http"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// MinIOObjectStorage represents MinIO object storage
type MinIOObjectStorage struct {
minIOClient *minio.Client
minIOConfig *settings.MinIOConfig
rootPath string
}
// NewMinIOObjectStorage returns a MinIO object storage
func NewMinIOObjectStorage(config *settings.Config, pathPrefix string) (*MinIOObjectStorage, error) {
minIOConfig := config.MinIOConfig
minIOClient, err := minio.New(minIOConfig.Endpoint, &minio.Options{
Region: minIOConfig.Location,
Creds: credentials.NewStaticV4(minIOConfig.AccessKeyID, minIOConfig.SecretAccessKey, ""),
Secure: minIOConfig.UseSSL,
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: minIOConfig.SkipTLSVerify}},
})
if err != nil {
return nil, err
}
storage := &MinIOObjectStorage{
minIOClient: minIOClient,
minIOConfig: minIOConfig,
rootPath: minIOConfig.RootPath,
}
storage.rootPath = storage.getFinalPath(pathPrefix)
ctx := context.Background()
exists, err := minIOClient.BucketExists(ctx, minIOConfig.Bucket)
if err != nil {
return nil, err
}
if !exists {
err := minIOClient.MakeBucket(ctx, minIOConfig.Bucket, minio.MakeBucketOptions{
Region: minIOConfig.Location,
})
if err != nil {
return nil, err
}
}
return storage, nil
}
// Exists returns whether the file exists
func (s *MinIOObjectStorage) Exists(path string) (bool, error) {
ctx := context.Background()
objectInfo, err := s.minIOClient.StatObject(ctx, s.minIOConfig.Bucket, s.getFinalPath(path), minio.StatObjectOptions{})
if err == nil && !objectInfo.IsDeleteMarker {
return true, nil
}
return false, err
}
// Read returns the object instance according to specified the file path
func (s *MinIOObjectStorage) Read(path string) (ObjectInStorage, error) {
ctx := context.Background()
return s.minIOClient.GetObject(ctx, s.minIOConfig.Bucket, s.getFinalPath(path), minio.GetObjectOptions{})
}
// Save returns whether save the object instance successfully
func (s *MinIOObjectStorage) Save(path string, object ObjectInStorage) error {
ctx := context.Background()
_, err := s.minIOClient.PutObject(ctx, s.minIOConfig.Bucket, s.getFinalPath(path), object, -1, minio.PutObjectOptions{})
return err
}
// Delete returns whether delete the object according to specified the file path successfully
func (s *MinIOObjectStorage) Delete(path string) error {
ctx := context.Background()
return s.minIOClient.RemoveObject(ctx, s.minIOConfig.Bucket, s.getFinalPath(path), minio.RemoveObjectOptions{})
}
func (s *MinIOObjectStorage) getFinalPath(path string) string {
rootPath := s.rootPath
if len(rootPath) < 1 || rootPath[len(rootPath)-1] != '/' {
rootPath = rootPath + "/"
}
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
return rootPath + path
}
-11
View File
@@ -1,11 +0,0 @@
package storage
import (
"io"
)
// ObjectInStorage represents the object instance in the storage
type ObjectInStorage interface {
io.ReadCloser
io.Seeker
}
-9
View File
@@ -1,9 +0,0 @@
package storage
// ObjectStorage represents an object storage to store file object
type ObjectStorage interface {
Exists(path string) (bool, error)
Read(path string) (ObjectInStorage, error)
Save(path string, object ObjectInStorage) error
Delete(path string) error
}
-61
View File
@@ -1,61 +0,0 @@
package storage
import (
"fmt"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
const avatarPathPrefix = "avatar"
// StorageContainer contains the current object storage
type StorageContainer struct {
AvatarCurrentStorage ObjectStorage
}
// Initialize a object storage container singleton instance
var (
Container = &StorageContainer{}
)
// InitializeStorageContainer initializes the current object storage according to the config
func InitializeStorageContainer(config *settings.Config) error {
if config.StorageType == settings.LocalFileSystemObjectStorageType {
avatarStorage, err := NewLocalFileSystemObjectStorage(config, avatarPathPrefix)
Container.AvatarCurrentStorage = avatarStorage
return err
} else if config.StorageType == settings.MinIOStorageType {
avatarStorage, err := NewMinIOObjectStorage(config, avatarPathPrefix)
Container.AvatarCurrentStorage = avatarStorage
return err
}
return errs.ErrInvalidStorageType
}
// ExistsAvatar returns whether the user avatar exists from the current object storage
func (s *StorageContainer) ExistsAvatar(uid int64, fileExtension string) (bool, error) {
return s.AvatarCurrentStorage.Exists(s.getUserAvatarPath(uid, fileExtension))
}
// ReadAvatar returns the user avatar from the current object storage
func (s *StorageContainer) ReadAvatar(uid int64, fileExtension string) (ObjectInStorage, error) {
return s.AvatarCurrentStorage.Read(s.getUserAvatarPath(uid, fileExtension))
}
// SaveAvatar returns whether save the user avatar into the current object storage successfully
func (s *StorageContainer) SaveAvatar(uid int64, object ObjectInStorage, fileExtension string) error {
return s.AvatarCurrentStorage.Save(s.getUserAvatarPath(uid, fileExtension), object)
}
// DeleteAvatar returns whether delete the user avatar from the current object storage successfully
func (s *StorageContainer) DeleteAvatar(uid int64, fileExtension string) error {
return s.AvatarCurrentStorage.Delete(s.getUserAvatarPath(uid, fileExtension))
}
func (s *StorageContainer) getUserAvatarPath(uid int64, fileExtension string) string {
return fmt.Sprintf("%d.%s", uid, fileExtension)
}
-2
View File
@@ -110,8 +110,6 @@ func getValidationErrorText(err validator.FieldError) string {
return errs.GetParameterInvalidCurrencyMessage(fieldName)
case "validHexRGBColor":
return errs.GetParameterInvalidHexRGBColorMessage(fieldName)
case "validAmountFilter":
return errs.GetParameterInvalidAmountFilterMessage(fieldName)
}
return errs.GetParameterInvalidMessage(fieldName)

Some files were not shown because too many files have changed in this diff Show More