Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c8bb5a0b7 | |||
| d3abb279e3 | |||
| 952731a2d4 | |||
| df23cb8cdd | |||
| 87a21a1a4f | |||
| 7c3c1bbd6a | |||
| 03c342f6f6 | |||
| b0e01d36ab | |||
| bb84e8af13 | |||
| f42ee9cf67 | |||
| 8a0232aedf | |||
| f3ccd3b66d | |||
| b690316aa7 | |||
| 8a0777be4c | |||
| 013f44f64a | |||
| 9f8dbf77df | |||
| 274fb8b4e2 | |||
| 48a06c6570 | |||
| 5485242baf | |||
| ec7c4c7461 | |||
| 2259719935 | |||
| f8fc955408 | |||
| 4684de9705 | |||
| 52bab6f726 | |||
| 765e64d96f | |||
| f93610b5e0 | |||
| 5d1480cabc | |||
| 5faf3bfe66 | |||
| 5cb7eca340 | |||
| 9a2f682379 | |||
| fd4036f0c8 | |||
| c854dbaab4 | |||
| 0c75ed47ac | |||
| fa467e72f9 | |||
| 93e05d5634 | |||
| 745efe1222 | |||
| e1dcf56ca9 | |||
| 3aa33a48e9 | |||
| 29547bccb1 | |||
| 4823760fd1 | |||
| 8584e84af9 | |||
| af586a0432 | |||
| ce752c992c | |||
| 7b49a9f142 | |||
| 2fc5e91cc4 | |||
| f6d03bf5df | |||
| a17a2cc377 | |||
| beea6fe733 | |||
| 85b05f9e7e | |||
| d3ab2b94b7 | |||
| b21fff5b15 | |||
| 234e7a55ff | |||
| d4cf8fe077 | |||
| 2b2a266533 | |||
| 4b35103e34 | |||
| 81a5585029 | |||
| d2b89e629a | |||
| cab86eec68 | |||
| 295f5cc14a | |||
| 6395e3b5c1 | |||
| a42c5fa988 | |||
| 46e275d843 | |||
| 13ada3575a | |||
| 3b0e0f1a3f | |||
| 512acc5a49 | |||
| 1f101fea3e | |||
| 83bd8f23f4 | |||
| af56c3057c | |||
| 53a8ad71c6 | |||
| 600ae2bd58 | |||
| 60b6ed51cd | |||
| 8a947ef224 | |||
| d936b64cf9 | |||
| ab828ebdab | |||
| b444e8ee31 | |||
| 45f1177a73 | |||
| 64e7dc5e12 | |||
| e62bebb7fa | |||
| 3990a072ca | |||
| a5bd12945d | |||
| 7938e7c7c8 | |||
| 130a157abc | |||
| cce19ae957 | |||
| e90340fec4 | |||
| 22061e535a | |||
| 23a85d6162 | |||
| 2cb47bfd75 | |||
| 3ce7f6e99a | |||
| 77b083c41b | |||
| b8fcdacb84 | |||
| d893193e73 | |||
| 73f8446d07 | |||
| 5692bec216 | |||
| e88491268b | |||
| 94cd5dc21a | |||
| 697f69d5d7 | |||
| 76fff27b3f | |||
| 4f664dbfc3 | |||
| d9b726cdf9 | |||
| cc792b9c0f | |||
| b916217b28 | |||
| 6c4b7059ed | |||
| 9d0e294ed2 | |||
| dc6d8398b1 | |||
| a50d2e7e72 | |||
| ea5cfe60f2 | |||
| 5b37ea4d78 | |||
| 46dd2888a6 | |||
| 564c9e1d95 | |||
| 4d0d3959a9 | |||
| e9e6644e7f | |||
| 6a19131ea1 |
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
@@ -25,14 +25,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
|
||||||
image: tonistiigi/binfmt:qemu-v8.1.5
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Set up the environment
|
- name: Set up the environment
|
||||||
|
id: setup
|
||||||
run: |
|
run: |
|
||||||
|
echo "build_unix_time=$(date '+%s')" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "build_date=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"
|
||||||
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
|
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
|
||||||
cat >> docker/custom-backend-pre-setup.sh <<EOF
|
cat >> docker/custom-backend-pre-setup.sh <<EOF
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
@@ -55,6 +56,8 @@ jobs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
RELEASE_BUILD=1
|
RELEASE_BUILD=1
|
||||||
BUILD_PIPELINE=1
|
BUILD_PIPELINE=1
|
||||||
|
BUILD_UNIXTIME=${{ steps.setup.outputs.build_unix_time }}
|
||||||
|
BUILD_DATE=${{ steps.setup.outputs.build_date }}
|
||||||
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
|
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
|
||||||
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
@@ -25,14 +25,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
|
||||||
image: tonistiigi/binfmt:qemu-v8.1.5
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Set up the environment
|
- name: Set up the environment
|
||||||
|
id: setup
|
||||||
run: |
|
run: |
|
||||||
|
echo "build_unix_time=$(date '+%s')" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "build_date=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"
|
||||||
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
|
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
|
||||||
cat >> docker/custom-backend-pre-setup.sh <<EOF
|
cat >> docker/custom-backend-pre-setup.sh <<EOF
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
@@ -54,6 +55,8 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
BUILD_PIPELINE=1
|
BUILD_PIPELINE=1
|
||||||
|
BUILD_UNIXTIME=${{ steps.setup.outputs.build_unix_time }}
|
||||||
|
BUILD_DATE=${{ steps.setup.outputs.build_date }}
|
||||||
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
|
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
|
||||||
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Report a bug in ezBookkeeping
|
description: Report a bug in ezBookkeeping
|
||||||
labels: bug
|
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checkboxes
|
id: checkboxes
|
||||||
|
|||||||
@@ -1 +1,8 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Frequently Asked Questions
|
||||||
|
url: https://ezbookkeeping.mayswind.net/faq
|
||||||
|
about: Please check whether your question has already been mentioned here.
|
||||||
|
- name: Usage Questions
|
||||||
|
url: https://github.com/mayswind/ezbookkeeping/discussions/categories/q-a
|
||||||
|
about: Questions about using ezBookkeeping can be discussed in GitHub Discussions.
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Request a feature or enhancement for ezBookkeeping
|
description: Request a feature or enhancement for ezBookkeeping
|
||||||
labels: enhancement
|
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checkboxes
|
id: checkboxes
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
name: Build docker image and package for linux
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
release-build:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
build-unix-time:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
build-date:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
check-3rd-api:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
skip-tests:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
platform:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
platform-name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
docker-push:
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
docker-image-name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
docker-username:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
docker-password:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
docker-bake-meta-file-path:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
docker-bake-meta-artifact-name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
docker-bake-digests-file-path:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
docker-bake-digests-artifact-name-prefix:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
package-file-name-prefix:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
package-artifact-name-prefix:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Download docker bake meta artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.docker-bake-meta-artifact-name }}
|
||||||
|
path: ${{ runner.temp }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
if: ${{ inputs.docker-username != '' && inputs.docker-password != '' }}
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ inputs.docker-username }}
|
||||||
|
password: ${{ inputs.docker-password }}
|
||||||
|
|
||||||
|
- name: Build docker for ${{ inputs.platform-name }}
|
||||||
|
id: bake
|
||||||
|
uses: docker/bake-action@v6
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
./docker-bake.hcl
|
||||||
|
cwd://${{ inputs.docker-bake-meta-file-path }}
|
||||||
|
source: .
|
||||||
|
targets: image
|
||||||
|
set: |
|
||||||
|
*.tags=${{ inputs.docker-image-name }}
|
||||||
|
*.platform=${{ inputs.platform }}
|
||||||
|
*.args.RELEASE_BUILD=${{ inputs.release-build }}
|
||||||
|
*.args.BUILD_PIPELINE=1
|
||||||
|
*.args.BUILD_UNIXTIME=${{ inputs.build-unix-time }}
|
||||||
|
*.args.BUILD_DATE=${{ inputs.build-date }}
|
||||||
|
*.args.CHECK_3RD_API=${{ inputs.check-3rd-api }}
|
||||||
|
*.args.SKIP_TESTS=${{ inputs.skip-tests }}
|
||||||
|
*.output=type=image,push-by-digest=true,name-canonical=true,push=${{ inputs.docker-push }}
|
||||||
|
*.output+=type=local,dest=${{ runner.temp }}/package
|
||||||
|
|
||||||
|
- name: Export digests file for ${{ inputs.platform-name }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
digest="${{ fromJSON(steps.bake.outputs.metadata).image['containerimage.digest'] }}"
|
||||||
|
mkdir -p ${{ inputs.docker-bake-digests-file-path }}
|
||||||
|
touch "${{ inputs.docker-bake-digests-file-path }}/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Build package file for ${{ inputs.platform-name }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cd ${{ runner.temp }}/package/ezbookkeeping
|
||||||
|
tar -czf ${{ runner.temp }}/${{ inputs.package-file-name-prefix }}-${{ inputs.platform-name }}.tar.gz *
|
||||||
|
|
||||||
|
- name: Upload ${{ inputs.platform-name }} digests artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.docker-bake-digests-artifact-name-prefix }}-${{ inputs.platform-name }}
|
||||||
|
path: ${{ inputs.docker-bake-digests-file-path }}/*
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload artifact for ${{ inputs.platform-name }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.package-artifact-name-prefix }}-${{ inputs.platform-name }}
|
||||||
|
path: ${{ runner.temp }}/${{ inputs.package-file-name-prefix }}-${{ inputs.platform-name }}.tar.gz
|
||||||
|
if-no-files-found: error
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: Build backend file for windows
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
go-version:
|
||||||
|
required: false
|
||||||
|
default: "1.25.3"
|
||||||
|
mingw-version:
|
||||||
|
required: false
|
||||||
|
default: "14.2.0"
|
||||||
|
mingw-revison:
|
||||||
|
required: false
|
||||||
|
default: "v12-rev2"
|
||||||
|
release-build:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
build-unix-time:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
build-date:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
check-3rd-api:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
skip-tests:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
backend-artifact-name-prefix:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: ${{ inputs.go-version }}
|
||||||
|
|
||||||
|
- name: Install MinGW
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$mingwVersion = "${{ inputs.mingw-version }}"
|
||||||
|
$mingwRevision = "${{ inputs.mingw-revison }}"
|
||||||
|
$url = "https://github.com/niXman/mingw-builds-binaries/releases/download/${mingwVersion}-rt_${mingwRevision}/x86_64-${mingwVersion}-release-posix-seh-ucrt-rt_${mingwRevision}.7z"
|
||||||
|
$archive = "C:\mingw.7z"
|
||||||
|
$mingwDir = "C:\mingw64"
|
||||||
|
|
||||||
|
Write-Host "Downloading MinGW from ${url}"
|
||||||
|
Invoke-WebRequest -Uri ${url} -OutFile ${archive}
|
||||||
|
|
||||||
|
Remove-Item -Recurse -Force ${mingwDir}
|
||||||
|
New-Item -ItemType Directory -Path ${mingwDir}
|
||||||
|
|
||||||
|
Write-Host "Extracting MinGW to ${mingwDir}"
|
||||||
|
7z x ${archive} -oC:\
|
||||||
|
"${mingwDir}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
|
||||||
|
- name: Build backend for windows-x64
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
RELEASE_BUILD: "${{ inputs.release-build }}"
|
||||||
|
BUILD_PIPELINE: "1"
|
||||||
|
BUILD_UNIXTIME: "${{ inputs.build-unix-time }}"
|
||||||
|
BUILD_DATE: "${{ inputs.build-date }}"
|
||||||
|
CHECK_3RD_API: "${{ inputs.check-3rd-api }}"
|
||||||
|
SKIP_TESTS: "${{ inputs.skip-tests }}"
|
||||||
|
run: |
|
||||||
|
.\build.ps1 backend
|
||||||
|
|
||||||
|
- name: Upload windows backend artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.backend-artifact-name-prefix }}-windows-x64
|
||||||
|
path: ezbookkeeping.exe
|
||||||
|
if-no-files-found: error
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
name: Build packages for windows
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
package-file-name-prefix:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
package-artifact-name-prefix:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
backend-artifact-name-prefix:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Download windows-x64 backend file
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.backend-artifact-name-prefix }}-windows-x64
|
||||||
|
path: ${{ runner.temp }}\backend
|
||||||
|
|
||||||
|
- name: Download linux-amd64 packaged files
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.package-artifact-name-prefix }}-linux-amd64
|
||||||
|
path: ${{ runner.temp }}\package
|
||||||
|
|
||||||
|
- name: Extract frontend files from linux-amd64 package
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
New-Item -ItemType Directory -Path package
|
||||||
|
tar -xzf (Get-ChildItem ${{ runner.temp }}\package\${{ inputs.package-file-name-prefix }}-linux-amd64.tar.gz) -C package
|
||||||
|
|
||||||
|
- name: Package windows-x64 package
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
New-Item -ItemType Directory -Path "ezbookkeeping"
|
||||||
|
New-Item -ItemType Directory -Path "ezbookkeeping\data"
|
||||||
|
New-Item -ItemType Directory -Path "ezbookkeeping\storage"
|
||||||
|
New-Item -ItemType Directory -Path "ezbookkeeping\log"
|
||||||
|
Copy-Item ${{ runner.temp }}\backend\ezbookkeeping.exe -Destination ezbookkeeping\
|
||||||
|
Copy-Item -Recurse -Force package\public -Destination ezbookkeeping\public
|
||||||
|
Copy-Item -Recurse -Force conf -Destination ezbookkeeping\conf
|
||||||
|
Copy-Item -Recurse -Force templates -Destination ezbookkeeping\templates
|
||||||
|
Copy-Item .\LICENSE -Destination ezbookkeeping\
|
||||||
|
Push-Location ezbookkeeping
|
||||||
|
7z a -r -tzip -mx9 ..\${{ inputs.package-file-name-prefix }}-windows-x64.zip *
|
||||||
|
Pop-Location
|
||||||
|
Remove-Item -Recurse -Force ezbookkeeping
|
||||||
|
|
||||||
|
- name: Upload windows artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.package-artifact-name-prefix }}-windows-x64
|
||||||
|
path: ${{ inputs.package-file-name-prefix }}-windows-x64.zip
|
||||||
|
if-no-files-found: error
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
name: Push linux docker multi-arch image to registry
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
docker-image-name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
docker-username:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
docker-password:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
docker-bake-meta-file-path:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
docker-bake-meta-artifact-name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
docker-bake-digests-file-path:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
docker-bake-digests-artifact-name-prefix:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
docker-image-tags:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Download docker bake meta artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.docker-bake-meta-artifact-name }}
|
||||||
|
path: ${{ runner.temp }}
|
||||||
|
|
||||||
|
- name: Download digests artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: ${{ inputs.docker-bake-digests-artifact-name-prefix }}-*
|
||||||
|
merge-multiple: true
|
||||||
|
path: ${{ inputs.docker-bake-digests-file-path }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ inputs.docker-username }}
|
||||||
|
password: ${{ inputs.docker-password }}
|
||||||
|
|
||||||
|
- name: Create manifest and push
|
||||||
|
shell: bash
|
||||||
|
working-directory: ${{ inputs.docker-bake-digests-file-path }}
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create $(echo "${{ inputs.docker-image-tags }}" | xargs -I {} echo -n " -t {}") $(printf '${{ inputs.docker-image-name }}@sha256:%s ' *)
|
||||||
@@ -6,27 +6,144 @@ on:
|
|||||||
- main
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-linux-docker:
|
setup:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
build-unix-time: ${{ steps.variable.outputs.build_unix_time }}
|
||||||
|
build-date: ${{ steps.variable.outputs.build_date }}
|
||||||
|
docker-tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
docker-labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
ezbookkeeping-docker-bake-meta-file-path: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}
|
||||||
|
ezbookkeeping-docker-bake-meta-artifact-name: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_artifact_name }}
|
||||||
|
ezbookkeeping-docker-digests-file-path: ${{ steps.variable.outputs.ezbookkeeping_docker_digests_file_path }}
|
||||||
|
ezbookkeeping-docker-digests-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_docker_digests_artifact_name_prefix }}
|
||||||
|
ezbookkeeping-package-file-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_package_file_name_prefix }}
|
||||||
|
ezbookkeeping-package-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_package_artifact_name_prefix }}
|
||||||
|
ezbookkeeping-backend-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_backend_artifact_name_prefix }}
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: Checkout
|
||||||
name: Checkout
|
uses: actions/checkout@v5
|
||||||
uses: actions/checkout@v4
|
|
||||||
-
|
- name: Docker meta
|
||||||
name: Login to DockerHub
|
id: meta
|
||||||
uses: docker/login-action@v3
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
images: |
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
${{ vars.DOCKER_IMAGE_NAME }}
|
||||||
-
|
tags: |
|
||||||
name: Build
|
type=raw,value=dev-${{ github.run_id }}
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
|
- name: Set up variables
|
||||||
|
id: variable
|
||||||
|
run: |
|
||||||
|
echo "build_unix_time=$(date '+%s')" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "build_date=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_docker_bake_meta_file_path=${{ runner.temp }}/bake-meta.json" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_docker_bake_meta_artifact_name=ezbookkeeping-build-dev-docker-bake-meta-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_docker_digests_file_path=${{ runner.temp }}/digests" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_docker_digests_artifact_name_prefix=ezbookkeeping-build-dev-digests-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_package_file_name_prefix=ezbookkeeping-dev-package-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_package_artifact_name_prefix=ezbookkeeping-build-dev-package-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_backend_artifact_name_prefix=ezbookkeeping-build-dev-backend-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Rename docker bake meta file
|
||||||
|
run: |
|
||||||
|
mv "${{ steps.meta.outputs.bake-file }}" "${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}"
|
||||||
|
|
||||||
|
- name: Upload docker bake meta artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
file: Dockerfile
|
name: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_artifact_name }}
|
||||||
context: .
|
path: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}
|
||||||
platforms: linux/amd64
|
if-no-files-found: error
|
||||||
push: false
|
|
||||||
build-args: |
|
build-linux-docker-and-package-x86:
|
||||||
BUILD_PIPELINE=1
|
runs-on: ubuntu-24.04
|
||||||
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
|
needs:
|
||||||
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
- setup
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- uses: ./.github/actions/build-linux-docker-and-package
|
||||||
|
with:
|
||||||
|
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
|
||||||
|
build-date: ${{ needs.setup.outputs.build-date }}
|
||||||
|
check-3rd-api: ${{ vars.CHECK_3RD_API }}
|
||||||
|
skip-tests: ${{ vars.SKIP_TESTS }}
|
||||||
|
platform: linux/amd64
|
||||||
|
platform-name: linux-amd64
|
||||||
|
docker-push: false
|
||||||
|
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
|
||||||
|
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
|
||||||
|
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
|
||||||
|
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
|
||||||
|
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
|
||||||
|
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
|
||||||
|
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
|
||||||
|
|
||||||
|
build-linux-docker-and-package-arm:
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
needs:
|
||||||
|
- setup
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: linux/arm64/v8
|
||||||
|
platform-name: linux-arm64
|
||||||
|
- platform: linux/arm/v7
|
||||||
|
platform-name: linux-armv7
|
||||||
|
- platform: linux/arm/v6
|
||||||
|
platform-name: linux-armv6
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- uses: ./.github/actions/build-linux-docker-and-package
|
||||||
|
with:
|
||||||
|
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
|
||||||
|
build-date: ${{ needs.setup.outputs.build-date }}
|
||||||
|
check-3rd-api: ${{ vars.CHECK_3RD_API }}
|
||||||
|
skip-tests: ${{ vars.SKIP_TESTS }}
|
||||||
|
platform: ${{ matrix.platform }}
|
||||||
|
platform-name: ${{ matrix.platform-name }}
|
||||||
|
docker-push: false
|
||||||
|
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
|
||||||
|
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
|
||||||
|
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
|
||||||
|
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
|
||||||
|
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
|
||||||
|
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
|
||||||
|
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
|
||||||
|
|
||||||
|
build-windows-backend:
|
||||||
|
needs:
|
||||||
|
- setup
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- uses: ./.github/actions/build-windows-backend
|
||||||
|
with:
|
||||||
|
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
|
||||||
|
build-date: ${{ needs.setup.outputs.build-date }}
|
||||||
|
check-3rd-api: ${{ vars.CHECK_3RD_API }}
|
||||||
|
skip-tests: ${{ vars.SKIP_TESTS }}
|
||||||
|
backend-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-backend-artifact-name-prefix }}
|
||||||
|
|
||||||
|
build-windows-package:
|
||||||
|
needs:
|
||||||
|
- setup
|
||||||
|
- build-windows-backend
|
||||||
|
- build-linux-docker-and-package-x86
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- uses: ./.github/actions/build-windows-package
|
||||||
|
with:
|
||||||
|
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
|
||||||
|
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
|
||||||
|
backend-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-backend-artifact-name-prefix }}
|
||||||
|
|||||||
+142
-162
@@ -6,214 +6,194 @@ on:
|
|||||||
- "v*.*.*"
|
- "v*.*.*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-linux-docker:
|
setup:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
image-tag: ${{ steps.meta.outputs.version }}
|
build-unix-time: ${{ steps.variable.outputs.build_unix_time }}
|
||||||
|
build-date: ${{ steps.variable.outputs.build_date }}
|
||||||
|
docker-version: ${{ steps.meta.outputs.version }}
|
||||||
|
docker-tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
docker-labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
ezbookkeeping-docker-bake-meta-file-path: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}
|
||||||
|
ezbookkeeping-docker-bake-meta-artifact-name: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_artifact_name }}
|
||||||
|
ezbookkeeping-docker-digests-file-path: ${{ steps.variable.outputs.ezbookkeeping_docker_digests_file_path }}
|
||||||
|
ezbookkeeping-docker-digests-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_docker_digests_artifact_name_prefix }}
|
||||||
|
ezbookkeeping-package-file-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_package_file_name_prefix }}
|
||||||
|
ezbookkeeping-package-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_package_artifact_name_prefix }}
|
||||||
|
ezbookkeeping-backend-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_backend_artifact_name_prefix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
|
${{ vars.DOCKER_IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up variables
|
||||||
uses: docker/setup-qemu-action@v3
|
id: variable
|
||||||
|
run: |
|
||||||
|
echo "build_unix_time=$(date '+%s')" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "build_date=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_docker_bake_meta_file_path=${{ runner.temp }}/bake-meta.json" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_docker_bake_meta_artifact_name=ezbookkeeping-build-release-docker-bake-meta-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_docker_digests_file_path=${{ runner.temp }}/digests" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_docker_digests_artifact_name_prefix=ezbookkeeping-build-release-digests-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_package_file_name_prefix=ezbookkeeping-${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_package_artifact_name_prefix=ezbookkeeping-build-release-package-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_backend_artifact_name_prefix=ezbookkeeping-build-release-backend-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Rename docker bake meta file
|
||||||
|
run: |
|
||||||
|
mv "${{ steps.meta.outputs.bake-file }}" "${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}"
|
||||||
|
|
||||||
|
- name: Upload docker bake meta artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
image: tonistiigi/binfmt:qemu-v8.1.5
|
name: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_artifact_name }}
|
||||||
|
path: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
build-linux-docker-and-package-x86:
|
||||||
uses: docker/setup-buildx-action@v3
|
runs-on: ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- setup
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- uses: ./.github/actions/build-linux-docker-and-package
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
release-build: 1
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
|
||||||
|
build-date: ${{ needs.setup.outputs.build-date }}
|
||||||
|
check-3rd-api: ${{ vars.CHECK_3RD_API }}
|
||||||
|
skip-tests: ${{ vars.SKIP_TESTS }}
|
||||||
|
platform: linux/amd64
|
||||||
|
platform-name: linux-amd64
|
||||||
|
docker-push: true
|
||||||
|
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
|
||||||
|
docker-username: ${{ vars.DOCKER_USERNAME }}
|
||||||
|
docker-password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
|
||||||
|
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
|
||||||
|
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
|
||||||
|
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
|
||||||
|
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
|
||||||
|
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
|
||||||
|
|
||||||
- name: Build and push
|
build-linux-docker-and-package-arm:
|
||||||
uses: docker/build-push-action@v6
|
runs-on: ubuntu-24.04-arm
|
||||||
with:
|
needs:
|
||||||
file: Dockerfile
|
- setup
|
||||||
context: .
|
|
||||||
platforms: |
|
|
||||||
linux/amd64
|
|
||||||
linux/arm64/v8
|
|
||||||
linux/arm/v7
|
|
||||||
linux/arm/v6
|
|
||||||
push: true
|
|
||||||
build-args: |
|
|
||||||
RELEASE_BUILD=1
|
|
||||||
BUILD_PIPELINE=1
|
|
||||||
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
|
|
||||||
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
|
|
||||||
upload-linux-artifact:
|
|
||||||
needs: build-linux-docker
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- arch: linux/amd64
|
- platform: linux/arm64/v8
|
||||||
arch_alias: linux-amd64
|
platform-name: linux-arm64
|
||||||
- arch: linux/arm64/v8
|
- platform: linux/arm/v7
|
||||||
arch_alias: linux-arm64
|
platform-name: linux-armv7
|
||||||
- arch: linux/arm/v7
|
- platform: linux/arm/v6
|
||||||
arch_alias: linux-armv7
|
platform-name: linux-armv6
|
||||||
- arch: linux/arm/v6
|
|
||||||
arch_alias: linux-armv6
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- uses: ./.github/actions/build-linux-docker-and-package
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
release-build: 1
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
|
||||||
|
build-date: ${{ needs.setup.outputs.build-date }}
|
||||||
|
check-3rd-api: ${{ vars.CHECK_3RD_API }}
|
||||||
|
skip-tests: ${{ vars.SKIP_TESTS }}
|
||||||
|
platform: ${{ matrix.platform }}
|
||||||
|
platform-name: ${{ matrix.platform-name }}
|
||||||
|
docker-push: true
|
||||||
|
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
|
||||||
|
docker-username: ${{ vars.DOCKER_USERNAME }}
|
||||||
|
docker-password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
|
||||||
|
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
|
||||||
|
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
|
||||||
|
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
|
||||||
|
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
|
||||||
|
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
|
||||||
|
|
||||||
- name: Pull and save packaged files for ${{ matrix.arch }}
|
push-linux-docker:
|
||||||
run: |
|
needs:
|
||||||
VERSION=${{ needs.build-linux-docker.outputs.image-tag }}
|
- setup
|
||||||
IMAGE=${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:${VERSION}
|
- build-linux-docker-and-package-x86
|
||||||
docker pull --platform ${{ matrix.arch }} ${IMAGE}
|
- build-linux-docker-and-package-arm
|
||||||
cid=$(docker create "${IMAGE}")
|
runs-on: ubuntu-latest
|
||||||
docker cp ${cid}:/ezbookkeeping ./ezbookkeeping
|
steps:
|
||||||
docker rm ${cid}
|
- name: Checkout
|
||||||
cd ezbookkeeping
|
uses: actions/checkout@v5
|
||||||
tar -czf ../ezbookkeeping-v${VERSION}-${{ matrix.arch_alias }}.tar.gz *
|
|
||||||
cd ..
|
|
||||||
rm -rf ezbookkeeping
|
|
||||||
|
|
||||||
- name: Upload artifact
|
- uses: ./.github/actions/push-linux-docker
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: ezbookkeeping-${{ github.ref_name }}-${{ matrix.arch_alias }}
|
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
|
||||||
path: ezbookkeeping-${{ github.ref_name }}-${{ matrix.arch_alias }}.tar.gz
|
docker-username: ${{ vars.DOCKER_USERNAME }}
|
||||||
if-no-files-found: error
|
docker-password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
|
||||||
|
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
|
||||||
|
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
|
||||||
|
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
|
||||||
|
docker-image-tags: ${{ needs.setup.outputs.docker-tags }}
|
||||||
|
|
||||||
build-and-upload-windows-package:
|
build-windows-backend:
|
||||||
needs: upload-linux-artifact
|
needs:
|
||||||
|
- setup
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
env:
|
|
||||||
GO_VERSION: "1.25.1"
|
|
||||||
MINGW_VERSION: "14.2.0"
|
|
||||||
MINGW_REVISION: "v12-rev2"
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Download linux-amd64 packaged files
|
- uses: ./.github/actions/build-windows-backend
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: ezbookkeeping-${{ github.ref_name }}-linux-amd64
|
release-build: 1
|
||||||
path: artifacts
|
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
|
||||||
|
build-date: ${{ needs.setup.outputs.build-date }}
|
||||||
|
check-3rd-api: ${{ vars.CHECK_3RD_API }}
|
||||||
|
skip-tests: ${{ vars.SKIP_TESTS }}
|
||||||
|
backend-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-backend-artifact-name-prefix }}
|
||||||
|
|
||||||
- name: Extract frontend files from linux-amd64 package
|
build-windows-package:
|
||||||
run: |
|
needs:
|
||||||
New-Item -ItemType Directory -Path package
|
- setup
|
||||||
tar -xzf (Get-ChildItem artifacts\ezbookkeeping-${{ github.ref_name }}-linux-amd64.tar.gz) -C package
|
- build-windows-backend
|
||||||
|
- build-linux-docker-and-package-x86
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Go
|
- uses: ./.github/actions/build-windows-package
|
||||||
uses: actions/setup-go@v6
|
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
|
||||||
|
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
|
||||||
- name: Install MinGW
|
backend-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-backend-artifact-name-prefix }}
|
||||||
run: |
|
|
||||||
$mingwVersion = "${{ env.MINGW_VERSION }}"
|
|
||||||
$mingwRevision = "${{ env.MINGW_REVISION }}"
|
|
||||||
$url = "https://github.com/niXman/mingw-builds-binaries/releases/download/${mingwVersion}-rt_${mingwRevision}/x86_64-${mingwVersion}-release-posix-seh-ucrt-rt_${mingwRevision}.7z"
|
|
||||||
$archive = "C:\mingw.7z"
|
|
||||||
$mingwDir = "C:\mingw64"
|
|
||||||
|
|
||||||
Write-Host "Downloading MinGW from ${url}"
|
|
||||||
Invoke-WebRequest -Uri ${url} -OutFile ${archive}
|
|
||||||
|
|
||||||
Remove-Item -Recurse -Force ${mingwDir}
|
|
||||||
New-Item -ItemType Directory -Path ${mingwDir}
|
|
||||||
|
|
||||||
Write-Host "Extracting MinGW to ${mingwDir}"
|
|
||||||
7z x ${archive} -oC:\
|
|
||||||
"${mingwDir}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
|
||||||
|
|
||||||
- name: Build backend for windows-x64
|
|
||||||
env:
|
|
||||||
RELEASE_BUILD: "1"
|
|
||||||
BUILD_PIPELINE: "1"
|
|
||||||
CHECK_3RD_API: ${{ vars.CHECK_3RD_API }}
|
|
||||||
SKIP_TESTS: ${{ vars.SKIP_TESTS }}
|
|
||||||
run: |
|
|
||||||
.\build.ps1 backend
|
|
||||||
|
|
||||||
- name: Package Windows build
|
|
||||||
run: |
|
|
||||||
New-Item -ItemType Directory -Path "ezbookkeeping"
|
|
||||||
New-Item -ItemType Directory -Path "ezbookkeeping\data"
|
|
||||||
New-Item -ItemType Directory -Path "ezbookkeeping\storage"
|
|
||||||
New-Item -ItemType Directory -Path "ezbookkeeping\log"
|
|
||||||
Copy-Item ezbookkeeping.exe -Destination ezbookkeeping\
|
|
||||||
Copy-Item -Recurse -Force package\public -Destination ezbookkeeping\public
|
|
||||||
Copy-Item -Recurse -Force conf -Destination ezbookkeeping\conf
|
|
||||||
Copy-Item -Recurse -Force templates -Destination ezbookkeeping\templates
|
|
||||||
Copy-Item .\LICENSE -Destination ezbookkeeping\
|
|
||||||
Push-Location ezbookkeeping
|
|
||||||
7z a -r -tzip -mx9 ..\ezbookkeeping-${{ github.ref_name }}-windows-x64.zip *
|
|
||||||
Pop-Location
|
|
||||||
Remove-Item -Recurse -Force ezbookkeeping
|
|
||||||
|
|
||||||
- name: Upload Windows artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ezbookkeeping-${{ github.ref_name }}-windows-x64
|
|
||||||
path: ezbookkeeping-${{ github.ref_name }}-windows-x64.zip
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
publish-release:
|
publish-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- upload-linux-artifact
|
- setup
|
||||||
- build-and-upload-windows-package
|
- build-linux-docker-and-package-x86
|
||||||
|
- build-linux-docker-and-package-arm
|
||||||
|
- build-windows-package
|
||||||
|
- push-linux-docker
|
||||||
steps:
|
steps:
|
||||||
- name: Download linux-amd64 packaged files
|
- name: Download all packaged files
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ezbookkeeping-${{ github.ref_name }}-linux-amd64
|
pattern: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}-*
|
||||||
path: ./release-files
|
merge-multiple: true
|
||||||
|
path: release-files
|
||||||
- name: Download linux-arm64 packaged files
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ezbookkeeping-${{ github.ref_name }}-linux-arm64
|
|
||||||
path: ./release-files
|
|
||||||
|
|
||||||
- name: Download linux-armv6 packaged files
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ezbookkeeping-${{ github.ref_name }}-linux-armv6
|
|
||||||
path: ./release-files
|
|
||||||
|
|
||||||
- name: Download linux-armv7 packaged files
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ezbookkeeping-${{ github.ref_name }}-linux-armv7
|
|
||||||
path: ./release-files
|
|
||||||
|
|
||||||
- name: Download windows-x64 packaged files
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ezbookkeeping-${{ github.ref_name }}-windows-x64
|
|
||||||
path: ./release-files
|
|
||||||
|
|
||||||
- name: Publish Release ${{ github.ref_name }}
|
- name: Publish Release ${{ github.ref_name }}
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
|
|||||||
@@ -6,173 +6,172 @@ on:
|
|||||||
- main
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-linux-docker:
|
setup:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
image-tag: ${{ steps.meta.outputs.version }}
|
build-unix-time: ${{ steps.variable.outputs.build_unix_time }}
|
||||||
|
build-date: ${{ steps.variable.outputs.build_date }}
|
||||||
|
docker-version: ${{ steps.meta.outputs.version }}
|
||||||
|
docker-tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
docker-labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
ezbookkeeping-docker-bake-meta-file-path: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}
|
||||||
|
ezbookkeeping-docker-bake-meta-artifact-name: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_artifact_name }}
|
||||||
|
ezbookkeeping-docker-digests-file-path: ${{ steps.variable.outputs.ezbookkeeping_docker_digests_file_path }}
|
||||||
|
ezbookkeeping-docker-digests-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_docker_digests_artifact_name_prefix }}
|
||||||
|
ezbookkeeping-package-file-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_package_file_name_prefix }}
|
||||||
|
ezbookkeeping-package-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_package_artifact_name_prefix }}
|
||||||
|
ezbookkeeping-backend-artifact-name-prefix: ${{ steps.variable.outputs.ezbookkeeping_backend_artifact_name_prefix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
|
${{ vars.DOCKER_IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}.${{ github.run_id }}
|
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}-${{ github.run_id }}
|
||||||
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}
|
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}
|
||||||
type=raw,value=latest-snapshot
|
type=raw,value=latest-snapshot
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up variables
|
||||||
uses: docker/setup-qemu-action@v3
|
id: variable
|
||||||
|
run: |
|
||||||
|
echo "build_unix_time=$(date '+%s')" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "build_date=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_docker_bake_meta_file_path=${{ runner.temp }}/bake-meta.json" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_docker_bake_meta_artifact_name=ezbookkeeping-build-dev-docker-bake-meta-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_docker_digests_file_path=${{ runner.temp }}/digests" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_docker_digests_artifact_name_prefix=ezbookkeeping-build-dev-digests-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_package_file_name_prefix=ezbookkeeping-dev-package-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_package_artifact_name_prefix=ezbookkeeping-build-dev-package-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ezbookkeeping_backend_artifact_name_prefix=ezbookkeeping-build-dev-backend-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Rename docker bake meta file
|
||||||
|
run: |
|
||||||
|
mv "${{ steps.meta.outputs.bake-file }}" "${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}"
|
||||||
|
|
||||||
|
- name: Upload docker bake meta artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
image: tonistiigi/binfmt:qemu-v8.1.5
|
name: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_artifact_name }}
|
||||||
|
path: ${{ steps.variable.outputs.ezbookkeeping_docker_bake_meta_file_path }}
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
build-linux-docker-and-package-x86:
|
||||||
uses: docker/setup-buildx-action@v3
|
runs-on: ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- setup
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- uses: ./.github/actions/build-linux-docker-and-package
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
build-date: ${{ needs.setup.outputs.build-date }}
|
||||||
|
check-3rd-api: ${{ vars.CHECK_3RD_API }}
|
||||||
|
skip-tests: ${{ vars.SKIP_TESTS }}
|
||||||
|
platform: linux/amd64
|
||||||
|
platform-name: linux-amd64
|
||||||
|
docker-push: true
|
||||||
|
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
|
||||||
|
docker-username: ${{ vars.DOCKER_USERNAME }}
|
||||||
|
docker-password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
|
||||||
|
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
|
||||||
|
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
|
||||||
|
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
|
||||||
|
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
|
||||||
|
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
|
||||||
|
|
||||||
- name: Build and push
|
build-linux-docker-and-package-arm:
|
||||||
uses: docker/build-push-action@v6
|
runs-on: ubuntu-24.04-arm
|
||||||
with:
|
needs:
|
||||||
file: Dockerfile
|
- setup
|
||||||
context: .
|
|
||||||
platforms: |
|
|
||||||
linux/amd64
|
|
||||||
linux/arm64/v8
|
|
||||||
linux/arm/v7
|
|
||||||
linux/arm/v6
|
|
||||||
push: true
|
|
||||||
build-args: |
|
|
||||||
BUILD_PIPELINE=1
|
|
||||||
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
|
|
||||||
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
|
|
||||||
upload-linux-artifact:
|
|
||||||
needs: build-linux-docker
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- arch: linux/amd64
|
- platform: linux/arm64/v8
|
||||||
arch_alias: linux-amd64
|
platform-name: linux-arm64
|
||||||
- arch: linux/arm64/v8
|
- platform: linux/arm/v7
|
||||||
arch_alias: linux-arm64
|
platform-name: linux-armv7
|
||||||
- arch: linux/arm/v7
|
- platform: linux/arm/v6
|
||||||
arch_alias: linux-armv7
|
platform-name: linux-armv6
|
||||||
- arch: linux/arm/v6
|
|
||||||
arch_alias: linux-armv6
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- uses: ./.github/actions/build-linux-docker-and-package
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
build-date: ${{ needs.setup.outputs.build-date }}
|
||||||
|
check-3rd-api: ${{ vars.CHECK_3RD_API }}
|
||||||
|
skip-tests: ${{ vars.SKIP_TESTS }}
|
||||||
|
platform: ${{ matrix.platform }}
|
||||||
|
platform-name: ${{ matrix.platform-name }}
|
||||||
|
docker-push: true
|
||||||
|
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
|
||||||
|
docker-username: ${{ vars.DOCKER_USERNAME }}
|
||||||
|
docker-password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
|
||||||
|
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
|
||||||
|
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
|
||||||
|
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
|
||||||
|
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
|
||||||
|
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
|
||||||
|
|
||||||
- name: Pull and save packaged files for ${{ matrix.arch }}
|
push-linux-docker:
|
||||||
run: |
|
needs:
|
||||||
TAG=${{ needs.build-linux-docker.outputs.image-tag }}
|
- setup
|
||||||
IMAGE=${{ secrets.DOCKER_USERNAME }}/ezbookkeeping:${TAG}
|
- build-linux-docker-and-package-x86
|
||||||
docker pull --platform ${{ matrix.arch }} ${IMAGE}
|
- build-linux-docker-and-package-arm
|
||||||
cid=$(docker create "${IMAGE}")
|
runs-on: ubuntu-latest
|
||||||
docker cp ${cid}:/ezbookkeeping ./ezbookkeeping
|
steps:
|
||||||
docker rm ${cid}
|
- name: Checkout
|
||||||
cd ezbookkeeping
|
uses: actions/checkout@v5
|
||||||
tar -czf ../ezbookkeeping-dev-${{ github.run_id }}-${{ matrix.arch_alias }}.tar.gz *
|
|
||||||
cd ..
|
|
||||||
rm -rf ezbookkeeping
|
|
||||||
|
|
||||||
- name: Upload artifact
|
- uses: ./.github/actions/push-linux-docker
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: ezbookkeeping-dev-${{ github.run_id }}-${{ matrix.arch_alias }}
|
docker-image-name: ${{ vars.DOCKER_IMAGE_NAME }}
|
||||||
path: ezbookkeeping-dev-${{ github.run_id }}-${{ matrix.arch_alias }}.tar.gz
|
docker-username: ${{ vars.DOCKER_USERNAME }}
|
||||||
if-no-files-found: error
|
docker-password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
docker-bake-meta-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-file-path }}
|
||||||
|
docker-bake-meta-artifact-name: ${{ needs.setup.outputs.ezbookkeeping-docker-bake-meta-artifact-name }}
|
||||||
|
docker-bake-digests-file-path: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-file-path }}
|
||||||
|
docker-bake-digests-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-docker-digests-artifact-name-prefix }}
|
||||||
|
docker-image-tags: ${{ needs.setup.outputs.docker-tags }}
|
||||||
|
|
||||||
build-and-upload-windows-package:
|
build-windows-backend:
|
||||||
needs: upload-linux-artifact
|
needs:
|
||||||
|
- setup
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
env:
|
|
||||||
GO_VERSION: "1.25.1"
|
|
||||||
MINGW_VERSION: "14.2.0"
|
|
||||||
MINGW_REVISION: "v12-rev2"
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Download linux-amd64 packaged files
|
- uses: ./.github/actions/build-windows-backend
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: ezbookkeeping-dev-${{ github.run_id }}-linux-amd64
|
build-unix-time: ${{ needs.setup.outputs.build-unix-time }}
|
||||||
path: artifacts
|
build-date: ${{ needs.setup.outputs.build-date }}
|
||||||
|
check-3rd-api: ${{ vars.CHECK_3RD_API }}
|
||||||
|
skip-tests: ${{ vars.SKIP_TESTS }}
|
||||||
|
backend-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-backend-artifact-name-prefix }}
|
||||||
|
|
||||||
- name: Extract frontend files from linux-amd64 package
|
build-windows-package:
|
||||||
run: |
|
needs:
|
||||||
New-Item -ItemType Directory -Path package
|
- setup
|
||||||
tar -xzf (Get-ChildItem artifacts\ezbookkeeping-dev-${{ github.run_id }}-linux-amd64.tar.gz) -C package
|
- build-windows-backend
|
||||||
|
- build-linux-docker-and-package-x86
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Go
|
- uses: ./.github/actions/build-windows-package
|
||||||
uses: actions/setup-go@v6
|
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
package-file-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-file-name-prefix }}
|
||||||
|
package-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-package-artifact-name-prefix }}
|
||||||
- name: Install MinGW
|
backend-artifact-name-prefix: ${{ needs.setup.outputs.ezbookkeeping-backend-artifact-name-prefix }}
|
||||||
run: |
|
|
||||||
$mingwVersion = "${{ env.MINGW_VERSION }}"
|
|
||||||
$mingwRevision = "${{ env.MINGW_REVISION }}"
|
|
||||||
$url = "https://github.com/niXman/mingw-builds-binaries/releases/download/${mingwVersion}-rt_${mingwRevision}/x86_64-${mingwVersion}-release-posix-seh-ucrt-rt_${mingwRevision}.7z"
|
|
||||||
$archive = "C:\mingw.7z"
|
|
||||||
$mingwDir = "C:\mingw64"
|
|
||||||
|
|
||||||
Write-Host "Downloading MinGW from ${url}"
|
|
||||||
Invoke-WebRequest -Uri ${url} -OutFile ${archive}
|
|
||||||
|
|
||||||
Remove-Item -Recurse -Force ${mingwDir}
|
|
||||||
New-Item -ItemType Directory -Path ${mingwDir}
|
|
||||||
|
|
||||||
Write-Host "Extracting MinGW to ${mingwDir}"
|
|
||||||
7z x ${archive} -oC:\
|
|
||||||
"${mingwDir}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
|
||||||
|
|
||||||
- name: Build backend for windows-x64
|
|
||||||
env:
|
|
||||||
BUILD_PIPELINE: "1"
|
|
||||||
CHECK_3RD_API: ${{ vars.CHECK_3RD_API }}
|
|
||||||
SKIP_TESTS: ${{ vars.SKIP_TESTS }}
|
|
||||||
run: |
|
|
||||||
.\build.ps1 backend
|
|
||||||
|
|
||||||
- name: Package Windows build
|
|
||||||
run: |
|
|
||||||
New-Item -ItemType Directory -Path "ezbookkeeping"
|
|
||||||
New-Item -ItemType Directory -Path "ezbookkeeping\data"
|
|
||||||
New-Item -ItemType Directory -Path "ezbookkeeping\storage"
|
|
||||||
New-Item -ItemType Directory -Path "ezbookkeeping\log"
|
|
||||||
Copy-Item ezbookkeeping.exe -Destination ezbookkeeping\
|
|
||||||
Copy-Item -Recurse -Force package\public -Destination ezbookkeeping\public
|
|
||||||
Copy-Item -Recurse -Force conf -Destination ezbookkeeping\conf
|
|
||||||
Copy-Item -Recurse -Force templates -Destination ezbookkeeping\templates
|
|
||||||
Copy-Item .\LICENSE -Destination ezbookkeeping\
|
|
||||||
Push-Location ezbookkeeping
|
|
||||||
7z a -r -tzip -mx9 ..\ezbookkeeping-dev-${{ github.run_id }}-windows-x64.zip *
|
|
||||||
Pop-Location
|
|
||||||
Remove-Item -Recurse -Force ezbookkeeping
|
|
||||||
|
|
||||||
- name: Upload Windows artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ezbookkeeping-dev-${{ github.run_id }}-windows-x64
|
|
||||||
path: ezbookkeeping-dev-${{ github.run_id }}-windows-x64.zip
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|||||||
+11
-3
@@ -1,11 +1,15 @@
|
|||||||
# Build backend binary file
|
# Build backend binary file
|
||||||
FROM golang:1.25.1-alpine3.22 AS be-builder
|
FROM golang:1.25.3-alpine3.22 AS be-builder
|
||||||
ARG RELEASE_BUILD
|
ARG RELEASE_BUILD
|
||||||
ARG BUILD_PIPELINE
|
ARG BUILD_PIPELINE
|
||||||
|
ARG BUILD_UNIXTIME
|
||||||
|
ARG BUILD_DATE
|
||||||
ARG CHECK_3RD_API
|
ARG CHECK_3RD_API
|
||||||
ARG SKIP_TESTS
|
ARG SKIP_TESTS
|
||||||
ENV RELEASE_BUILD=$RELEASE_BUILD
|
ENV RELEASE_BUILD=$RELEASE_BUILD
|
||||||
ENV BUILD_PIPELINE=$BUILD_PIPELINE
|
ENV BUILD_PIPELINE=$BUILD_PIPELINE
|
||||||
|
ENV BUILD_UNIXTIME=$BUILD_UNIXTIME
|
||||||
|
ENV BUILD_DATE=$BUILD_DATE
|
||||||
ENV CHECK_3RD_API=$CHECK_3RD_API
|
ENV CHECK_3RD_API=$CHECK_3RD_API
|
||||||
ENV SKIP_TESTS=$SKIP_TESTS
|
ENV SKIP_TESTS=$SKIP_TESTS
|
||||||
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
||||||
@@ -15,11 +19,15 @@ RUN apk add git gcc g++ libc-dev
|
|||||||
RUN ./build.sh backend
|
RUN ./build.sh backend
|
||||||
|
|
||||||
# Build frontend files
|
# Build frontend files
|
||||||
FROM --platform=$BUILDPLATFORM node:24.7.0-alpine3.22 AS fe-builder
|
FROM --platform=$BUILDPLATFORM node:24.10.0-alpine3.22 AS fe-builder
|
||||||
ARG RELEASE_BUILD
|
ARG RELEASE_BUILD
|
||||||
ARG BUILD_PIPELINE
|
ARG BUILD_PIPELINE
|
||||||
|
ARG BUILD_UNIXTIME
|
||||||
|
ARG BUILD_DATE
|
||||||
ENV RELEASE_BUILD=$RELEASE_BUILD
|
ENV RELEASE_BUILD=$RELEASE_BUILD
|
||||||
ENV BUILD_PIPELINE=$BUILD_PIPELINE
|
ENV BUILD_PIPELINE=$BUILD_PIPELINE
|
||||||
|
ENV BUILD_UNIXTIME=$BUILD_UNIXTIME
|
||||||
|
ENV BUILD_DATE=$BUILD_DATE
|
||||||
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN docker/frontend-build-pre-setup.sh
|
RUN docker/frontend-build-pre-setup.sh
|
||||||
@@ -27,7 +35,7 @@ RUN apk add git
|
|||||||
RUN ./build.sh frontend
|
RUN ./build.sh frontend
|
||||||
|
|
||||||
# Package docker image
|
# Package docker image
|
||||||
FROM alpine:3.22.1
|
FROM alpine:3.22.2
|
||||||
LABEL maintainer="MaysWind <i@mayswind.net>"
|
LABEL maintainer="MaysWind <i@mayswind.net>"
|
||||||
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
|
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
|
||||||
RUN apk --no-cache add tzdata
|
RUN apk --no-cache add tzdata
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ Download the latest release: [https://github.com/mayswind/ezbookkeeping/releases
|
|||||||
By default, ezBookkeeping listens on port 8080. You can then visit `http://{YOUR_HOST_ADDRESS}:8080/` .
|
By default, ezBookkeeping listens on port 8080. You can then visit `http://{YOUR_HOST_ADDRESS}:8080/` .
|
||||||
|
|
||||||
### Build from Source
|
### 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:
|
Make sure you have [Golang](https://golang.org/), [GCC](https://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**
|
**Linux / macOS**
|
||||||
|
|
||||||
@@ -133,6 +133,7 @@ Currently available translations:
|
|||||||
| fr | Français | [@brieucdlf](https://github.com/brieucdlf) |
|
| fr | Français | [@brieucdlf](https://github.com/brieucdlf) |
|
||||||
| it | Italiano | [@waron97](https://github.com/waron97) |
|
| it | Italiano | [@waron97](https://github.com/waron97) |
|
||||||
| ja | 日本語 | [@tkymmm](https://github.com/tkymmm) |
|
| ja | 日本語 | [@tkymmm](https://github.com/tkymmm) |
|
||||||
|
| ko | 한국어 | [@overworks](https://github.com/overworks) |
|
||||||
| nl | Nederlands | [@automagic](https://github.com/automagics) |
|
| nl | Nederlands | [@automagic](https://github.com/automagics) |
|
||||||
| pt-BR | Português (Brasil) | [@thecodergus](https://github.com/thecodergus) |
|
| pt-BR | Português (Brasil) | [@thecodergus](https://github.com/thecodergus) |
|
||||||
| ru | Русский | [@artegoser](https://github.com/artegoser) |
|
| ru | Русский | [@artegoser](https://github.com/artegoser) |
|
||||||
@@ -145,8 +146,8 @@ Currently available translations:
|
|||||||
Don't see your language? Help us add it.
|
Don't see your language? Help us add it.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
1. [English](http://ezbookkeeping.mayswind.net)
|
1. [English](https://ezbookkeeping.mayswind.net)
|
||||||
1. [中文 (简体)](http://ezbookkeeping.mayswind.net/zh_Hans)
|
1. [中文 (简体)](https://ezbookkeeping.mayswind.net/zh_Hans)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
[MIT](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
|
[MIT](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ set "RELEASE=%RELEASE_BUILD%"
|
|||||||
set "RELEASE_TYPE=unknown"
|
set "RELEASE_TYPE=unknown"
|
||||||
set "VERSION="
|
set "VERSION="
|
||||||
set "COMMIT_HASH="
|
set "COMMIT_HASH="
|
||||||
set "BUILD_UNIXTIME="
|
set "BUILD_UNIXTIME=%BUILD_UNIXTIME%"
|
||||||
set "BUILD_DATE="
|
set "BUILD_DATE=%BUILD_DATE%"
|
||||||
set "PACKAGE_FILENAME="
|
set "PACKAGE_FILENAME="
|
||||||
for /f %%a in ('"prompt $E$S & echo on & for %%b in (1) do rem"') do set "ESC=%%a"
|
for /f %%a in ('"prompt $E$S & echo on & for %%b in (1) do rem"') do set "ESC=%%a"
|
||||||
|
|
||||||
@@ -113,8 +113,14 @@ goto :pre_parse_args
|
|||||||
set VERSION=%VERSION:,=%
|
set VERSION=%VERSION:,=%
|
||||||
set VERSION=%VERSION:"=%
|
set VERSION=%VERSION:"=%
|
||||||
for /f %%x in ('git rev-parse --short^=7 HEAD') do set "COMMIT_HASH=%%x"
|
for /f %%x in ('git rev-parse --short^=7 HEAD') do set "COMMIT_HASH=%%x"
|
||||||
call :set_unixtime BUILD_UNIXTIME
|
|
||||||
call :set_date BUILD_DATE
|
if "%BUILD_UNIXTIME%"=="" (
|
||||||
|
call :set_unixtime BUILD_UNIXTIME
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%BUILD_DATE%"=="" (
|
||||||
|
call :set_date BUILD_DATE
|
||||||
|
)
|
||||||
|
|
||||||
:main
|
:main
|
||||||
if "%TYPE%"=="backend" call :build_backend & goto :end
|
if "%TYPE%"=="backend" call :build_backend & goto :end
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ $script:SkipTests = $env:SKIP_TESTS
|
|||||||
$script:ReleaseType = "unknown"
|
$script:ReleaseType = "unknown"
|
||||||
$script:Version = ""
|
$script:Version = ""
|
||||||
$script:CommitHash = ""
|
$script:CommitHash = ""
|
||||||
$script:BuildUnixTime = ""
|
$script:BuildUnixTime = $env:BUILD_UNIXTIME
|
||||||
$script:BuildDate = ""
|
$script:BuildDate = $env:BUILD_DATE
|
||||||
|
|
||||||
function Write-Red($msg) {
|
function Write-Red($msg) {
|
||||||
Write-Host $msg -ForegroundColor Red
|
Write-Host $msg -ForegroundColor Red
|
||||||
@@ -79,8 +79,14 @@ function Check-Type-Dependencies {
|
|||||||
function Set-Build-Parameters {
|
function Set-Build-Parameters {
|
||||||
$script:Version = (Get-Content package.json | ConvertFrom-Json).version
|
$script:Version = (Get-Content package.json | ConvertFrom-Json).version
|
||||||
$script:CommitHash = git rev-parse --short=7 HEAD
|
$script:CommitHash = git rev-parse --short=7 HEAD
|
||||||
$script:BuildUnixTime = [int][double]::Parse((Get-Date -UFormat %s))
|
|
||||||
$script:BuildDate = Get-Date -Format "yyyyMMdd"
|
if (-not $BuildUnixTime) {
|
||||||
|
$script:BuildUnixTime = [int][double]::Parse((Get-Date -UFormat %s))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $BuildDate) {
|
||||||
|
$script:BuildDate = Get-Date -Format "yyyyMMdd"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Build-Backend {
|
function Build-Backend {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ RELEASE=${RELEASE_BUILD:-"0"}
|
|||||||
RELEASE_TYPE="unknown"
|
RELEASE_TYPE="unknown"
|
||||||
VERSION=""
|
VERSION=""
|
||||||
COMMIT_HASH=""
|
COMMIT_HASH=""
|
||||||
BUILD_UNIXTIME=""
|
BUILD_UNIXTIME="${BUILD_UNIXTIME}"
|
||||||
|
BUILD_DATE="${BUILD_DATE}"
|
||||||
PACKAGE_FILENAME=""
|
PACKAGE_FILENAME=""
|
||||||
DOCKER_TAG=""
|
DOCKER_TAG=""
|
||||||
|
|
||||||
@@ -118,7 +119,14 @@ check_type_dependencies() {
|
|||||||
set_build_parameters() {
|
set_build_parameters() {
|
||||||
VERSION="$(grep '"version": ' package.json | awk -F ':' '{print $2}' | tr -d ' ' | tr -d ',' | tr -d '"')"
|
VERSION="$(grep '"version": ' package.json | awk -F ':' '{print $2}' | tr -d ' ' | tr -d ',' | tr -d '"')"
|
||||||
COMMIT_HASH="$(git rev-parse --short=7 HEAD)"
|
COMMIT_HASH="$(git rev-parse --short=7 HEAD)"
|
||||||
BUILD_UNIXTIME="$(date '+%s')"
|
|
||||||
|
if [ -z "$BUILD_UNIXTIME" ]; then
|
||||||
|
BUILD_UNIXTIME="$(date '+%s')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$BUILD_DATE" ]; then
|
||||||
|
BUILD_DATE="$(date '+%Y%m%d')"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
build_backend() {
|
build_backend() {
|
||||||
@@ -203,7 +211,7 @@ build_package() {
|
|||||||
package_file_name="$VERSION";
|
package_file_name="$VERSION";
|
||||||
|
|
||||||
if [ "$RELEASE" = "0" ]; then
|
if [ "$RELEASE" = "0" ]; then
|
||||||
package_file_name="$package_file_name-$(date '+%Y%m%d')"
|
package_file_name="$package_file_name-$BUILD_DATE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
package_file_name="ezbookkeeping-$package_file_name-$(arch).tar.gz"
|
package_file_name="ezbookkeeping-$package_file_name-$(arch).tar.gz"
|
||||||
@@ -237,7 +245,7 @@ build_docker() {
|
|||||||
docker_tag="$VERSION"
|
docker_tag="$VERSION"
|
||||||
|
|
||||||
if [ "$RELEASE" = "0" ]; then
|
if [ "$RELEASE" = "0" ]; then
|
||||||
docker_tag="SNAPSHOT-$(date '+%Y%m%d')";
|
docker_tag="SNAPSHOT-$BUILD_DATE";
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker_tag="ezbookkeeping:$docker_tag"
|
docker_tag="ezbookkeeping:$docker_tag"
|
||||||
|
|||||||
@@ -149,5 +149,13 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
|||||||
|
|
||||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] user application cloud settings table maintained successfully")
|
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] user application cloud settings table maintained successfully")
|
||||||
|
|
||||||
|
err = datastore.Container.UserDataStore.SyncStructs(new(models.UserExternalAuth))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] user external auth table maintained successfully")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-9
@@ -162,20 +162,46 @@ func getConfigWithoutSensitiveData(config *settings.Config) *settings.Config {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
clonedConfig.DatabaseConfig.DatabasePassword = "****"
|
if clonedConfig.DatabaseConfig.DatabasePassword != "" {
|
||||||
clonedConfig.SMTPConfig.SMTPPasswd = "****"
|
clonedConfig.DatabaseConfig.DatabasePassword = "****"
|
||||||
clonedConfig.MinIOConfig.SecretAccessKey = "****"
|
}
|
||||||
clonedConfig.SecretKey = "****"
|
|
||||||
clonedConfig.AmapApplicationSecret = "****"
|
|
||||||
|
|
||||||
if clonedConfig.WebDAVConfig != nil {
|
if clonedConfig.SMTPConfig.SMTPPasswd != "" {
|
||||||
|
clonedConfig.SMTPConfig.SMTPPasswd = "****"
|
||||||
|
}
|
||||||
|
|
||||||
|
if clonedConfig.MinIOConfig.SecretAccessKey != "" {
|
||||||
|
clonedConfig.MinIOConfig.SecretAccessKey = "****"
|
||||||
|
}
|
||||||
|
|
||||||
|
if clonedConfig.SecretKey != "" {
|
||||||
|
clonedConfig.SecretKey = "****"
|
||||||
|
}
|
||||||
|
|
||||||
|
if clonedConfig.AmapApplicationSecret != "" {
|
||||||
|
clonedConfig.AmapApplicationSecret = "****"
|
||||||
|
}
|
||||||
|
|
||||||
|
if clonedConfig.WebDAVConfig != nil && clonedConfig.WebDAVConfig.Password != "" {
|
||||||
clonedConfig.WebDAVConfig.Password = "****"
|
clonedConfig.WebDAVConfig.Password = "****"
|
||||||
}
|
}
|
||||||
|
|
||||||
if clonedConfig.ReceiptImageRecognitionLLMConfig != nil {
|
if clonedConfig.ReceiptImageRecognitionLLMConfig != nil {
|
||||||
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAIAPIKey = "****"
|
if clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAIAPIKey != "" {
|
||||||
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAICompatibleAPIKey = "****"
|
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAIAPIKey = "****"
|
||||||
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey = "****"
|
}
|
||||||
|
|
||||||
|
if clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAICompatibleAPIKey != "" {
|
||||||
|
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAICompatibleAPIKey = "****"
|
||||||
|
}
|
||||||
|
|
||||||
|
if clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey != "" {
|
||||||
|
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey = "****"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if clonedConfig.OAuth2ClientSecret != "" {
|
||||||
|
clonedConfig.OAuth2ClientSecret = "****"
|
||||||
}
|
}
|
||||||
|
|
||||||
return clonedConfig
|
return clonedConfig
|
||||||
|
|||||||
+16
-4
@@ -264,7 +264,13 @@ var UserData = &cli.Command{
|
|||||||
Name: "type",
|
Name: "type",
|
||||||
Aliases: []string{"t"},
|
Aliases: []string{"t"},
|
||||||
Required: false,
|
Required: false,
|
||||||
Usage: "Specific token type, supports \"normal\" and \"mcp\", default is \"normal\"",
|
Usage: "Specific token type, supports \"api\" and \"mcp\", default is \"api\"",
|
||||||
|
},
|
||||||
|
&cli.Int64Flag{
|
||||||
|
Name: "expiresInSeconds",
|
||||||
|
Aliases: []string{"e"},
|
||||||
|
Required: true,
|
||||||
|
Usage: "Token expiration time in seconds (0 - 4294967295, 0 means no expiration).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -722,17 +728,23 @@ func createNewUserToken(c *core.CliContext) error {
|
|||||||
|
|
||||||
username := c.String("username")
|
username := c.String("username")
|
||||||
tokenType := c.String("type")
|
tokenType := c.String("type")
|
||||||
|
expiresInSeconds := c.Int64("expiresInSeconds")
|
||||||
|
|
||||||
if tokenType == "" {
|
if tokenType == "" {
|
||||||
tokenType = "normal"
|
tokenType = "api"
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokenType != "normal" && tokenType != "mcp" {
|
if tokenType != "api" && tokenType != "mcp" {
|
||||||
log.CliErrorf(c, "[user_data.createNewUserToken] token type is invalid")
|
log.CliErrorf(c, "[user_data.createNewUserToken] token type is invalid")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
token, tokenString, err := clis.UserData.CreateNewUserToken(c, username, tokenType)
|
if expiresInSeconds < 0 || expiresInSeconds > 4294967295 {
|
||||||
|
log.CliErrorf(c, "[user_data.createNewUserToken] expiresInSeconds is out of range (0 - 4294967295)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
token, tokenString, err := clis.UserData.CreateNewUserToken(c, username, tokenType, expiresInSeconds)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.CliErrorf(c, "[user_data.createNewUserToken] error occurs when creating user token")
|
log.CliErrorf(c, "[user_data.createNewUserToken] error occurs when creating user token")
|
||||||
|
|||||||
+64
-13
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/api"
|
"github.com/mayswind/ezbookkeeping/pkg/api"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/cron"
|
"github.com/mayswind/ezbookkeeping/pkg/cron"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
@@ -72,6 +73,13 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = oauth2.InitializeOAuth2Provider(config)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.BootErrorf(c, "[webserver.startWebServer] initializes oauth 2.0 provider failed, because %s", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = cron.InitializeCronJobSchedulerContainer(c, config, true)
|
err = cron.InitializeCronJobSchedulerContainer(c, config, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -104,6 +112,7 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
_ = v.RegisterValidation("notBlank", validators.NotBlank)
|
_ = v.RegisterValidation("notBlank", validators.NotBlank)
|
||||||
_ = v.RegisterValidation("validUsername", validators.ValidUsername)
|
_ = v.RegisterValidation("validUsername", validators.ValidUsername)
|
||||||
_ = v.RegisterValidation("validEmail", validators.ValidEmail)
|
_ = v.RegisterValidation("validEmail", validators.ValidEmail)
|
||||||
|
_ = v.RegisterValidation("validNickname", validators.ValidNickname)
|
||||||
_ = v.RegisterValidation("validCurrency", validators.ValidCurrency)
|
_ = v.RegisterValidation("validCurrency", validators.ValidCurrency)
|
||||||
_ = v.RegisterValidation("validHexRGBColor", validators.ValidHexRGBColor)
|
_ = v.RegisterValidation("validHexRGBColor", validators.ValidHexRGBColor)
|
||||||
_ = v.RegisterValidation("validAmountFilter", validators.ValidAmountFilter)
|
_ = v.RegisterValidation("validAmountFilter", validators.ValidAmountFilter)
|
||||||
@@ -167,7 +176,7 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
|
|
||||||
if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL {
|
if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL {
|
||||||
avatarRoute := router.Group("/avatar")
|
avatarRoute := router.Group("/avatar")
|
||||||
avatarRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
avatarRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString(config)))
|
||||||
{
|
{
|
||||||
avatarRoute.GET("/:fileName", bindImage(api.Users.UserGetAvatarHandler))
|
avatarRoute.GET("/:fileName", bindImage(api.Users.UserGetAvatarHandler))
|
||||||
}
|
}
|
||||||
@@ -175,7 +184,7 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
|
|
||||||
if config.EnableTransactionPictures {
|
if config.EnableTransactionPictures {
|
||||||
pictureRoute := router.Group("/pictures")
|
pictureRoute := router.Group("/pictures")
|
||||||
pictureRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
pictureRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString(config)))
|
||||||
{
|
{
|
||||||
pictureRoute.GET("/:fileName", bindImage(api.TransactionPictures.TransactionPictureGetHandler))
|
pictureRoute.GET("/:fileName", bindImage(api.TransactionPictures.TransactionPictureGetHandler))
|
||||||
}
|
}
|
||||||
@@ -184,7 +193,7 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler))
|
router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler))
|
||||||
|
|
||||||
proxyRoute := router.Group("/proxy")
|
proxyRoute := router.Group("/proxy")
|
||||||
proxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
|
proxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString(config)))
|
||||||
{
|
{
|
||||||
if config.EnableMapDataFetchProxy {
|
if config.EnableMapDataFetchProxy {
|
||||||
if config.MapProvider == settings.OpenStreetMapProvider ||
|
if config.MapProvider == settings.OpenStreetMapProvider ||
|
||||||
@@ -208,7 +217,7 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
|
|
||||||
if config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationInternalProxyMethod {
|
if config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationInternalProxyMethod {
|
||||||
amapApiProxyRoute := router.Group("/_AMapService")
|
amapApiProxyRoute := router.Group("/_AMapService")
|
||||||
amapApiProxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByCookie))
|
amapApiProxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByCookie(config)))
|
||||||
{
|
{
|
||||||
amapApiProxyRoute.GET("/*action", bindProxy(api.AmapApis.AmapApiProxyHandler))
|
amapApiProxyRoute.GET("/*action", bindProxy(api.AmapApis.AmapApiProxyHandler))
|
||||||
}
|
}
|
||||||
@@ -226,7 +235,7 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
mcpRoute.Use(bindMiddleware(middlewares.RequestId(config)))
|
mcpRoute.Use(bindMiddleware(middlewares.RequestId(config)))
|
||||||
mcpRoute.Use(bindMiddleware(middlewares.RequestLog))
|
mcpRoute.Use(bindMiddleware(middlewares.RequestLog))
|
||||||
mcpRoute.Use(bindMiddleware(middlewares.MCPServerIpLimit(config)))
|
mcpRoute.Use(bindMiddleware(middlewares.MCPServerIpLimit(config)))
|
||||||
mcpRoute.Use(bindMiddleware(middlewares.JWTMCPAuthorization))
|
mcpRoute.Use(bindMiddleware(middlewares.JWTMCPAuthorization(config)))
|
||||||
{
|
{
|
||||||
mcpRoute.POST("", bindJSONRPCApi(map[string]core.JSONRPCApiHandlerFunc{
|
mcpRoute.POST("", bindJSONRPCApi(map[string]core.JSONRPCApiHandlerFunc{
|
||||||
"initialize": api.ModelContextProtocols.InitializeHandler,
|
"initialize": api.ModelContextProtocols.InitializeHandler,
|
||||||
@@ -242,23 +251,43 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.EnableOAuth2Login {
|
||||||
|
oauth2Route := router.Group("/oauth2")
|
||||||
|
oauth2Route.Use(bindMiddleware(middlewares.RequestId(config)))
|
||||||
|
oauth2Route.Use(bindMiddleware(middlewares.RequestLog))
|
||||||
|
{
|
||||||
|
oauth2Route.GET("/login", bindRedirect(api.OAuth2Authentications.LoginHandler))
|
||||||
|
oauth2Route.GET("/callback", bindRedirect(api.OAuth2Authentications.CallbackHandler))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
apiRoute := router.Group("/api")
|
apiRoute := router.Group("/api")
|
||||||
|
|
||||||
apiRoute.Use(bindMiddleware(middlewares.RequestId(config)))
|
apiRoute.Use(bindMiddleware(middlewares.RequestId(config)))
|
||||||
apiRoute.Use(bindMiddleware(middlewares.RequestLog))
|
apiRoute.Use(bindMiddleware(middlewares.RequestLog))
|
||||||
{
|
{
|
||||||
apiRoute.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.AuthorizeHandler, config))
|
if config.EnableInternalAuth {
|
||||||
|
apiRoute.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.AuthorizeHandler, config))
|
||||||
|
}
|
||||||
|
|
||||||
if config.EnableTwoFactor {
|
if config.EnableInternalAuth && config.EnableTwoFactor {
|
||||||
twoFactorRoute := apiRoute.Group("/2fa")
|
twoFactorRoute := apiRoute.Group("/2fa")
|
||||||
twoFactorRoute.Use(bindMiddleware(middlewares.JWTTwoFactorAuthorization))
|
twoFactorRoute.Use(bindMiddleware(middlewares.JWTTwoFactorAuthorization(config)))
|
||||||
{
|
{
|
||||||
twoFactorRoute.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.TwoFactorAuthorizeHandler, config))
|
twoFactorRoute.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.TwoFactorAuthorizeHandler, config))
|
||||||
twoFactorRoute.POST("/recovery.json", bindApiWithTokenUpdate(api.Authorizations.TwoFactorAuthorizeByRecoveryCodeHandler, config))
|
twoFactorRoute.POST("/recovery.json", bindApiWithTokenUpdate(api.Authorizations.TwoFactorAuthorizeByRecoveryCodeHandler, config))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.EnableUserRegister {
|
if config.EnableOAuth2Login {
|
||||||
|
oauth2Route := apiRoute.Group("/oauth2")
|
||||||
|
oauth2Route.Use(bindMiddleware(middlewares.JWTOAuth2CallbackAuthorization(config)))
|
||||||
|
{
|
||||||
|
oauth2Route.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.OAuth2CallbackAuthorizeHandler, config))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.EnableInternalAuth && config.EnableUserRegister {
|
||||||
apiRoute.POST("/register.json", bindApiWithTokenUpdate(api.Users.UserRegisterHandler, config))
|
apiRoute.POST("/register.json", bindApiWithTokenUpdate(api.Users.UserRegisterHandler, config))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,17 +295,17 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
apiRoute.POST("/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByUnloginUserHandler))
|
apiRoute.POST("/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByUnloginUserHandler))
|
||||||
|
|
||||||
emailVerifyRoute := apiRoute.Group("/verify_email")
|
emailVerifyRoute := apiRoute.Group("/verify_email")
|
||||||
emailVerifyRoute.Use(bindMiddleware(middlewares.JWTEmailVerifyAuthorization))
|
emailVerifyRoute.Use(bindMiddleware(middlewares.JWTEmailVerifyAuthorization(config)))
|
||||||
{
|
{
|
||||||
emailVerifyRoute.POST("/by_token.json", bindApi(api.Users.UserEmailVerifyHandler))
|
emailVerifyRoute.POST("/by_token.json", bindApi(api.Users.UserEmailVerifyHandler))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.EnableUserForgetPassword {
|
if config.EnableInternalAuth && config.EnableUserForgetPassword {
|
||||||
apiRoute.POST("/forget_password/request.json", bindApi(api.ForgetPasswords.UserForgetPasswordRequestHandler))
|
apiRoute.POST("/forget_password/request.json", bindApi(api.ForgetPasswords.UserForgetPasswordRequestHandler))
|
||||||
|
|
||||||
resetPasswordRoute := apiRoute.Group("/forget_password/reset")
|
resetPasswordRoute := apiRoute.Group("/forget_password/reset")
|
||||||
resetPasswordRoute.Use(bindMiddleware(middlewares.JWTResetPasswordAuthorization))
|
resetPasswordRoute.Use(bindMiddleware(middlewares.JWTResetPasswordAuthorization(config)))
|
||||||
{
|
{
|
||||||
resetPasswordRoute.POST("/by_token.json", bindApi(api.ForgetPasswords.UserResetPasswordHandler))
|
resetPasswordRoute.POST("/by_token.json", bindApi(api.ForgetPasswords.UserResetPasswordHandler))
|
||||||
}
|
}
|
||||||
@@ -285,10 +314,11 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
apiRoute.GET("/logout.json", bindApiWithTokenUpdate(api.Tokens.TokenRevokeCurrentHandler, config))
|
apiRoute.GET("/logout.json", bindApiWithTokenUpdate(api.Tokens.TokenRevokeCurrentHandler, config))
|
||||||
|
|
||||||
apiV1Route := apiRoute.Group("/v1")
|
apiV1Route := apiRoute.Group("/v1")
|
||||||
apiV1Route.Use(bindMiddleware(middlewares.JWTAuthorization))
|
apiV1Route.Use(bindMiddleware(middlewares.JWTAuthorization(config)))
|
||||||
{
|
{
|
||||||
// Tokens
|
// Tokens
|
||||||
apiV1Route.GET("/tokens/list.json", bindApi(api.Tokens.TokenListHandler))
|
apiV1Route.GET("/tokens/list.json", bindApi(api.Tokens.TokenListHandler))
|
||||||
|
apiV1Route.POST("/tokens/generate/api.json", bindApi(api.Tokens.TokenGenerateAPIHandler))
|
||||||
apiV1Route.POST("/tokens/generate/mcp.json", bindApi(api.Tokens.TokenGenerateMCPHandler))
|
apiV1Route.POST("/tokens/generate/mcp.json", bindApi(api.Tokens.TokenGenerateMCPHandler))
|
||||||
apiV1Route.POST("/tokens/revoke.json", bindApi(api.Tokens.TokenRevokeHandler))
|
apiV1Route.POST("/tokens/revoke.json", bindApi(api.Tokens.TokenRevokeHandler))
|
||||||
apiV1Route.POST("/tokens/revoke_all.json", bindApi(api.Tokens.TokenRevokeAllHandler))
|
apiV1Route.POST("/tokens/revoke_all.json", bindApi(api.Tokens.TokenRevokeAllHandler))
|
||||||
@@ -307,6 +337,12 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
apiV1Route.POST("/users/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByLoginedUserHandler))
|
apiV1Route.POST("/users/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByLoginedUserHandler))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// External Authentications
|
||||||
|
if config.EnableOAuth2Login {
|
||||||
|
apiV1Route.GET("/users/external_auth/list.json", bindApi(api.UserExternalAuths.ExternalAuthListHanlder))
|
||||||
|
apiV1Route.POST("/users/external_auth/unlink.json", bindApi(api.UserExternalAuths.UnlinkExternalAuthHandler))
|
||||||
|
}
|
||||||
|
|
||||||
// Application Cloud Settings
|
// Application Cloud Settings
|
||||||
apiV1Route.GET("/users/settings/cloud/get.json", bindApi(api.UserApplicationCloudSettings.ApplicationSettingsGetHandler))
|
apiV1Route.GET("/users/settings/cloud/get.json", bindApi(api.UserApplicationCloudSettings.ApplicationSettingsGetHandler))
|
||||||
apiV1Route.POST("/users/settings/cloud/update.json", bindApi(api.UserApplicationCloudSettings.ApplicationSettingsUpdateHandler))
|
apiV1Route.POST("/users/settings/cloud/update.json", bindApi(api.UserApplicationCloudSettings.ApplicationSettingsUpdateHandler))
|
||||||
@@ -349,10 +385,12 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
apiV1Route.GET("/transactions/reconciliation_statements.json", bindApi(api.Transactions.TransactionReconciliationStatementHandler))
|
apiV1Route.GET("/transactions/reconciliation_statements.json", bindApi(api.Transactions.TransactionReconciliationStatementHandler))
|
||||||
apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler))
|
apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler))
|
||||||
apiV1Route.GET("/transactions/statistics/trends.json", bindApi(api.Transactions.TransactionStatisticsTrendsHandler))
|
apiV1Route.GET("/transactions/statistics/trends.json", bindApi(api.Transactions.TransactionStatisticsTrendsHandler))
|
||||||
|
apiV1Route.GET("/transactions/statistics/asset_trends.json", bindApi(api.Transactions.TransactionStatisticsAssetTrendsHandler))
|
||||||
apiV1Route.GET("/transactions/amounts.json", bindApi(api.Transactions.TransactionAmountsHandler))
|
apiV1Route.GET("/transactions/amounts.json", bindApi(api.Transactions.TransactionAmountsHandler))
|
||||||
apiV1Route.GET("/transactions/get.json", bindApi(api.Transactions.TransactionGetHandler))
|
apiV1Route.GET("/transactions/get.json", bindApi(api.Transactions.TransactionGetHandler))
|
||||||
apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler))
|
apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler))
|
||||||
apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler))
|
apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler))
|
||||||
|
apiV1Route.POST("/transactions/move/all.json", bindApi(api.Transactions.TransactionMoveAllBetweenAccountsHandler))
|
||||||
apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler))
|
apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler))
|
||||||
|
|
||||||
if config.EnableDataImport {
|
if config.EnableDataImport {
|
||||||
@@ -443,6 +481,19 @@ func bindMiddleware(fn core.MiddlewareHandlerFunc) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func bindRedirect(fn core.RedirectHandlerFunc) gin.HandlerFunc {
|
||||||
|
return func(ginCtx *gin.Context) {
|
||||||
|
c := core.WrapWebContext(ginCtx)
|
||||||
|
url, err := fn(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.PrintJsonErrorResult(c, err)
|
||||||
|
} else {
|
||||||
|
c.Redirect(http.StatusFound, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func bindApi(fn core.ApiHandlerFunc) gin.HandlerFunc {
|
func bindApi(fn core.ApiHandlerFunc) gin.HandlerFunc {
|
||||||
return func(ginCtx *gin.Context) {
|
return func(ginCtx *gin.Context) {
|
||||||
c := core.WrapWebContext(ginCtx)
|
c := core.WrapWebContext(ginCtx)
|
||||||
|
|||||||
+67
-3
@@ -263,6 +263,9 @@ email_verify_token_expired_time = 3600
|
|||||||
# Password reset token expired seconds (60 - 4294967295), default is 3600 (60 minutes)
|
# Password reset token expired seconds (60 - 4294967295), default is 3600 (60 minutes)
|
||||||
password_reset_token_expired_time = 3600
|
password_reset_token_expired_time = 3600
|
||||||
|
|
||||||
|
# Set to true to enable API token generation
|
||||||
|
enable_api_token = false
|
||||||
|
|
||||||
# Maximum count of password / token check failures (0 - 4294967295) per IP per minute (use the above duplicate checker), default is 5, set to 0 to disable
|
# Maximum count of password / token check failures (0 - 4294967295) per IP per minute (use the above duplicate checker), default is 5, set to 0 to disable
|
||||||
max_failures_per_ip_per_minute = 5
|
max_failures_per_ip_per_minute = 5
|
||||||
|
|
||||||
@@ -270,15 +273,72 @@ max_failures_per_ip_per_minute = 5
|
|||||||
max_failures_per_user_per_minute = 5
|
max_failures_per_user_per_minute = 5
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
# Set to true to enable two-factor authorization
|
# Set to true to enable internal authentication
|
||||||
|
enable_internal_auth = true
|
||||||
|
|
||||||
|
# Set to true to enable OAuth 2.0 authentication
|
||||||
|
enable_oauth2_auth = false
|
||||||
|
|
||||||
|
# For "internal" authentication only, set to true to enable two-factor authorization
|
||||||
enable_two_factor = true
|
enable_two_factor = true
|
||||||
|
|
||||||
# Set to true to allow users to reset password
|
# For "internal" authentication only, set to true to allow users to reset password
|
||||||
enable_forget_password = true
|
enable_forget_password = true
|
||||||
|
|
||||||
# Set to true to require email must be verified when use forget password
|
# For "internal" authentication only, set to true to require email must be verified when use forget password
|
||||||
forget_password_require_email_verify = false
|
forget_password_require_email_verify = false
|
||||||
|
|
||||||
|
# For "oauth2" authentication only, OAuth 2.0 provider, supports "oidc", "nextcloud", "gitea" and "github" currently
|
||||||
|
oauth2_provider =
|
||||||
|
|
||||||
|
# For "oauth2" authentication only, OAuth 2.0 client ID
|
||||||
|
oauth2_client_id =
|
||||||
|
|
||||||
|
# For "oauth2" authentication only, OAuth 2.0 client secret
|
||||||
|
oauth2_client_secret =
|
||||||
|
|
||||||
|
# For "oauth2" authentication only, OAuth 2.0 provider user identifier claim name, supports "email" and "username", default is "email"
|
||||||
|
oauth2_user_identifier = email
|
||||||
|
|
||||||
|
# For "oauth2" authentication only, set to true to use PKCE
|
||||||
|
oauth2_use_pkce = false
|
||||||
|
|
||||||
|
# For "oauth2" authentication only, if the user returned by OAuth 2.0 is not registered, automatically create a new user (requires "enable_register" to be set to true)
|
||||||
|
oauth2_auto_register = true
|
||||||
|
|
||||||
|
# For "oauth2" authentication only, OAuth 2.0 state expired seconds (60 - 4294967295), default is 300 (5 minutes)
|
||||||
|
oauth2_state_expired_time = 300
|
||||||
|
|
||||||
|
# For "oauth2" authentication only, requesting OAuth 2.0 api timeout (0 - 4294967295 milliseconds)
|
||||||
|
# Set to 0 to disable timeout for requesting OAuth 2.0 api, default is 10000 (10 seconds)
|
||||||
|
oauth2_request_timeout = 10000
|
||||||
|
|
||||||
|
# For "oauth2" authentication only, proxy for ezbookkeeping server requesting OAuth 2.0 api, supports "system" (use system proxy), "none" (do not use proxy), or proxy URL which starts with "http://", "https://" or "socks5://", default is "system"
|
||||||
|
oauth2_proxy = system
|
||||||
|
|
||||||
|
# For "oauth2" authentication only, set to true to skip tls verification when request OAuth 2.0 api
|
||||||
|
oauth2_skip_tls_verify = false
|
||||||
|
|
||||||
|
# For "oauth2" authentication and "oidc" OAuth 2.0 provider only, OIDC provider issuer url. Make sure the ".well-known" directory is available under this path. For example, if it's set to "https://auth.example.com", the discovery URL should be "https://auth.example.com/.well-known/openid-configuration".
|
||||||
|
oidc_provider_base_url =
|
||||||
|
|
||||||
|
# For "oauth2" authentication and "oidc" OAuth 2.0 provider only, set to true to check whether the issuer url in the discovery response matches the above "oidc_provider_base_url"
|
||||||
|
oidc_provider_check_issuer_url = true
|
||||||
|
|
||||||
|
# For "oauth2" authentication and "oidc" OAuth 2.0 provider only, set to true to replace the text "Connect ID" in the "Log in with Connect ID" button with the below custom provider name
|
||||||
|
enable_oidc_display_name = false
|
||||||
|
|
||||||
|
# For "oauth2" authentication and "oidc" OAuth 2.0 provider only, the custom provider name to replace the text in the "Log in with Connect ID" button, it supports multi-language configuration
|
||||||
|
# Add an underscore and a language tag after the setting key to configure the display name in that language
|
||||||
|
# For example, oidc_custom_display_name_zh_hans means the display name in Chinese (Simplified)
|
||||||
|
oidc_custom_display_name =
|
||||||
|
|
||||||
|
# For "oauth2" authentication and "nextcloud" OAuth 2.0 provider only, Nextcloud base url, e.g. "https://cloud.example.org/"
|
||||||
|
nextcloud_base_url =
|
||||||
|
|
||||||
|
# For "oauth2" authentication and "gitea" OAuth 2.0 provider only, Gitea base url, e.g. "https://git.example.com/"
|
||||||
|
gitea_base_url =
|
||||||
|
|
||||||
[user]
|
[user]
|
||||||
# Set to true to allow users to register account by themselves
|
# Set to true to allow users to register account by themselves
|
||||||
enable_register = true
|
enable_register = true
|
||||||
@@ -322,6 +382,10 @@ max_user_avatar_size = 1048576
|
|||||||
# 11: Clear All Data
|
# 11: Clear All Data
|
||||||
# 12: Sync Application Settings
|
# 12: Sync Application Settings
|
||||||
# 13: MCP (Model Context Protocol) Access
|
# 13: MCP (Model Context Protocol) Access
|
||||||
|
# 14: Create Transactions from AI Image Recognition
|
||||||
|
# 15: OAuth 2.0 Login
|
||||||
|
# 16: Unlink Third-party Login
|
||||||
|
# 17: Generate API Token
|
||||||
default_feature_restrictions =
|
default_feature_restrictions =
|
||||||
|
|
||||||
[data]
|
[data]
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
variable "DEFAULT_TAG" {
|
||||||
|
default = "ezbookkeeping:local"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special target: https://github.com/docker/metadata-action#bake-definition
|
||||||
|
target "docker-metadata-action" {
|
||||||
|
tags = ["${DEFAULT_TAG}"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default target if none specified
|
||||||
|
group "default" {
|
||||||
|
targets = ["image-local"]
|
||||||
|
}
|
||||||
|
|
||||||
|
target "image" {
|
||||||
|
inherits = ["docker-metadata-action"]
|
||||||
|
context = "./"
|
||||||
|
dockerfile = "Dockerfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
target "image-local" {
|
||||||
|
inherits = ["image"]
|
||||||
|
output = ["type=docker"]
|
||||||
|
}
|
||||||
|
|
||||||
|
target "image-all" {
|
||||||
|
inherits = ["image"]
|
||||||
|
platforms = [
|
||||||
|
"linux/amd64",
|
||||||
|
"linux/arm64",
|
||||||
|
"linux/arm/v7",
|
||||||
|
"linux/arm/v6"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=ezBookkeeping, a lightweight, self-hosted personal finance app with a sleek, user-friendly interface and powerful bookkeeping features.
|
Description=ezBookkeeping, a lightweight, self-hosted personal finance app with a user-friendly interface and powerful bookkeeping features.
|
||||||
After=syslog.target
|
After=syslog.target
|
||||||
After=network.target
|
After=network.target
|
||||||
After=mariadb.service mysqld.service postgresql.service
|
After=mariadb.service mysqld.service postgresql.service
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ go 1.25
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/boombuler/barcode v1.1.0
|
github.com/boombuler/barcode v1.1.0
|
||||||
|
github.com/coreos/go-oidc/v3 v3.16.0
|
||||||
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b
|
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b
|
||||||
github.com/gin-contrib/cache v1.4.1
|
github.com/gin-contrib/cache v1.4.1
|
||||||
github.com/gin-contrib/gzip v1.2.3
|
github.com/gin-contrib/gzip v1.2.5
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/go-co-op/gocron/v2 v2.16.5
|
github.com/go-co-op/gocron/v2 v2.17.0
|
||||||
github.com/go-playground/validator/v10 v10.27.0
|
github.com/go-playground/validator/v10 v10.28.0
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/invopop/jsonschema v0.13.0
|
github.com/invopop/jsonschema v0.13.0
|
||||||
@@ -20,16 +21,17 @@ require (
|
|||||||
github.com/pquerna/otp v1.5.0
|
github.com/pquerna/otp v1.5.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/urfave/cli/v3 v3.4.1
|
github.com/urfave/cli/v3 v3.5.0
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8
|
github.com/wk8/go-ordered-map/v2 v2.1.8
|
||||||
github.com/xuri/excelize/v2 v2.9.0
|
github.com/xuri/excelize/v2 v2.10.0
|
||||||
golang.org/x/crypto v0.41.0
|
golang.org/x/crypto v0.43.0
|
||||||
golang.org/x/net v0.43.0
|
golang.org/x/net v0.46.0
|
||||||
golang.org/x/text v0.28.0
|
golang.org/x/oauth2 v0.32.0
|
||||||
|
golang.org/x/text v0.30.0
|
||||||
gopkg.in/ini.v1 v1.67.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
gopkg.in/mail.v2 v2.3.1
|
gopkg.in/mail.v2 v2.3.1
|
||||||
xorm.io/builder v0.3.13
|
xorm.io/builder v0.3.13
|
||||||
xorm.io/xorm v1.3.10
|
xorm.io/xorm v1.3.11
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -37,31 +39,34 @@ require (
|
|||||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
|
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
|
||||||
github.com/buger/jsonparser v1.1.1 // indirect
|
github.com/buger/jsonparser v1.1.1 // indirect
|
||||||
github.com/bytedance/sonic v1.13.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/sonic v1.14.1 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a // indirect
|
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a // indirect
|
||||||
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 // indirect
|
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-ini/ini v1.67.0 // indirect
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/gomodule/redigo v1.9.2 // indirect
|
github.com/gomodule/redigo v1.9.2 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // 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.20 // indirect
|
||||||
@@ -74,6 +79,8 @@ require (
|
|||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||||
github.com/richardlehane/msoleps v1.0.4 // indirect
|
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
@@ -82,17 +89,20 @@ require (
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||||
github.com/tealeg/xlsx v1.0.5 // indirect
|
github.com/tealeg/xlsx v1.0.5 // indirect
|
||||||
github.com/tiendc/go-deepcopy v1.6.0 // indirect
|
github.com/tiendc/go-deepcopy v1.7.1 // indirect
|
||||||
github.com/tinylib/msgp v1.3.0 // indirect
|
github.com/tinylib/msgp v1.3.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.14 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
github.com/xuri/efp v0.0.1 // indirect
|
github.com/xuri/efp v0.0.1 // indirect
|
||||||
github.com/xuri/nfp v0.0.1 // indirect
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||||
golang.org/x/arch v0.18.0 // indirect
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/mod v0.29.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbT
|
|||||||
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
||||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
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/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||||
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
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.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
||||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/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-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
@@ -24,9 +25,11 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583j
|
|||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
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.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||||
github.com/chenzhuoyu/iasm v0.9.1/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.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@@ -40,30 +43,34 @@ github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7/go.mod h1:GPpMrAfHdb8
|
|||||||
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b h1:jqW/h4gcXYEB6kVf6iuxjU9ONWA0ugUB94TP9UNmgdg=
|
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b h1:jqW/h4gcXYEB6kVf6iuxjU9ONWA0ugUB94TP9UNmgdg=
|
||||||
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b/go.mod h1:iACcgahst7BboCpIMSpnFs4SKyU9ZjsvZBfNbUxZOJI=
|
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b/go.mod h1:iACcgahst7BboCpIMSpnFs4SKyU9ZjsvZBfNbUxZOJI=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/cache v1.4.1 h1:HcLwLfw7p+FasNp5VAnFbbBj9SzB4bDtswvon7wYSg4=
|
github.com/gin-contrib/cache v1.4.1 h1:HcLwLfw7p+FasNp5VAnFbbBj9SzB4bDtswvon7wYSg4=
|
||||||
github.com/gin-contrib/cache v1.4.1/go.mod h1:tykDV+FgItJHYEO0eCasuRYsZKPPyb4BYhAjuTlG6RM=
|
github.com/gin-contrib/cache v1.4.1/go.mod h1:tykDV+FgItJHYEO0eCasuRYsZKPPyb4BYhAjuTlG6RM=
|
||||||
github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U=
|
github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=
|
||||||
github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c=
|
github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
github.com/go-co-op/gocron/v2 v2.16.5 h1:j228Jxk7bb9CF8LKR3gS+bK3rcjRUINjlVI+ZMp26Ss=
|
github.com/go-co-op/gocron/v2 v2.17.0 h1:e/oj6fcAM8vOOKZxv2Cgfmjo+s8AXC46po5ZPtaSea4=
|
||||||
github.com/go-co-op/gocron/v2 v2.16.5/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc=
|
github.com/go-co-op/gocron/v2 v2.17.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI=
|
||||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
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/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
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/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
@@ -87,8 +94,8 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt
|
|||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
@@ -116,7 +123,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
|
||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
@@ -131,6 +137,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
|
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||||
|
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||||
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||||
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
@@ -159,44 +169,53 @@ github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFd
|
|||||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||||
github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
|
github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
|
||||||
github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
|
github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
|
||||||
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
|
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
|
||||||
|
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||||
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
||||||
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
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/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw=
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
github.com/ugorji/go/codec v1.2.14/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
|
github.com/urfave/cli/v3 v3.5.0 h1:qCuFMmdayTF3zmjG8TSsoBzrDqszNrklYg2x3g4MSgw=
|
||||||
github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
|
github.com/urfave/cli/v3 v3.5.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
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/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||||
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||||
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
|
github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4=
|
||||||
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
|
github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
|
||||||
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||||
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||||
|
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||||
|
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||||
|
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||||
|
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||||
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
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/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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -215,5 +234,5 @@ nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYm
|
|||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
||||||
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||||
xorm.io/xorm v1.3.10 h1:yR83hTT4mKIPyA/lvWFTzS35xjLwkiYnwdw0Qupeh0o=
|
xorm.io/xorm v1.3.11 h1:i4tlVUASogb0ZZFJHA7dZqoRU2pUpUsutnNdaOlFyMI=
|
||||||
xorm.io/xorm v1.3.10/go.mod h1:Lo7hmsFF0F0GbDE7ubX5ZKa+eCf0eCuiJAUG3oI5cxQ=
|
xorm.io/xorm v1.3.11/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q=
|
||||||
|
|||||||
Generated
+1093
-1099
File diff suppressed because it is too large
Load Diff
+22
-22
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ezbookkeeping",
|
"name": "ezbookkeeping",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -20,13 +20,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@vuepic/vue-datepicker": "^11.0.2",
|
"@vuepic/vue-datepicker": "^11.0.3",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.12.2",
|
||||||
"cbor-js": "^0.1.0",
|
"cbor-js": "^0.1.0",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dom7": "^4.0.6",
|
"dom7": "^4.0.6",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^6.0.0",
|
||||||
"framework7": "^8.3.4",
|
"framework7": "^8.3.4",
|
||||||
"framework7-icons": "^5.0.5",
|
"framework7-icons": "^5.0.5",
|
||||||
"framework7-vue": "^8.3.4",
|
"framework7-vue": "^8.3.4",
|
||||||
@@ -40,42 +40,42 @@
|
|||||||
"skeleton-elements": "^4.0.1",
|
"skeleton-elements": "^4.0.1",
|
||||||
"swiper": "^10.2.0",
|
"swiper": "^10.2.0",
|
||||||
"ua-parser-js": "^1.0.39",
|
"ua-parser-js": "^1.0.39",
|
||||||
"vue": "^3.5.21",
|
"vue": "^3.5.22",
|
||||||
"vue-echarts": "^7.0.3",
|
"vue-echarts": "^8.0.1",
|
||||||
"vue-i18n": "^11.1.12",
|
"vue-i18n": "^11.1.12",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.6.3",
|
||||||
"vue3-perfect-scrollbar": "^2.0.0",
|
"vue3-perfect-scrollbar": "^2.0.0",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
"vuetify": "^3.9.7"
|
"vuetify": "^3.10.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "^30.1.2",
|
"@jest/globals": "^30.2.0",
|
||||||
"@tsconfig/node24": "^24.0.1",
|
"@tsconfig/node24": "^24.0.1",
|
||||||
"@types/cbor-js": "^0.1.1",
|
"@types/cbor-js": "^0.1.1",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/git-rev-sync": "^2.0.2",
|
"@types/git-rev-sync": "^2.0.2",
|
||||||
"@types/jalaali-js": "^1.2.0",
|
"@types/jalaali-js": "^1.2.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^24.3.1",
|
"@types/node": "^24.9.1",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/eslint-config-typescript": "^14.6.0",
|
"@vue/eslint-config-typescript": "^14.6.0",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.38.0",
|
||||||
"eslint-plugin-vue": "^10.4.0",
|
"eslint-plugin-vue": "^10.5.1",
|
||||||
"git-rev-sync": "^3.0.2",
|
"git-rev-sync": "^3.0.2",
|
||||||
"jest": "^30.1.3",
|
"jest": "^30.2.0",
|
||||||
"postcss-preset-env": "^10.3.1",
|
"postcss-preset-env": "^10.4.0",
|
||||||
"sass": "^1.92.1",
|
"sass": "^1.93.2",
|
||||||
"ts-jest": "^29.4.1",
|
"ts-jest": "^29.4.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.1.4",
|
"vite": "^7.1.12",
|
||||||
"vite-plugin-checker": "^0.10.3",
|
"vite-plugin-checker": "^0.11.0",
|
||||||
"vite-plugin-pwa": "^1.0.3",
|
"vite-plugin-pwa": "^1.1.0",
|
||||||
"vite-plugin-vuetify": "^2.1.2",
|
"vite-plugin-vuetify": "^2.1.2",
|
||||||
"vue-tsc": "^3.0.6"
|
"vue-tsc": "^3.1.2"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 5 Chrome versions",
|
"last 5 Chrome versions",
|
||||||
|
|||||||
+201
-3
@@ -1,6 +1,9 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||||
@@ -22,6 +25,7 @@ type AuthorizationsApi struct {
|
|||||||
userAppCloudSettings *services.UserApplicationCloudSettingsService
|
userAppCloudSettings *services.UserApplicationCloudSettingsService
|
||||||
tokens *services.TokenService
|
tokens *services.TokenService
|
||||||
twoFactorAuthorizations *services.TwoFactorAuthorizationService
|
twoFactorAuthorizations *services.TwoFactorAuthorizationService
|
||||||
|
userExternalAuths *services.UserExternalAuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a authorization api singleton instance
|
// Initialize a authorization api singleton instance
|
||||||
@@ -48,11 +52,16 @@ var (
|
|||||||
userAppCloudSettings: services.UserApplicationCloudSettings,
|
userAppCloudSettings: services.UserApplicationCloudSettings,
|
||||||
tokens: services.Tokens,
|
tokens: services.Tokens,
|
||||||
twoFactorAuthorizations: services.TwoFactorAuthorizations,
|
twoFactorAuthorizations: services.TwoFactorAuthorizations,
|
||||||
|
userExternalAuths: services.UserExternalAuths,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthorizeHandler verifies and authorizes current login request
|
// AuthorizeHandler verifies and authorizes current login request
|
||||||
func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Error) {
|
func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
|
if !a.CurrentConfig().EnableInternalAuth {
|
||||||
|
return nil, errs.ErrCannotLoginByPassword
|
||||||
|
}
|
||||||
|
|
||||||
var credential models.UserLoginRequest
|
var credential models.UserLoginRequest
|
||||||
err := c.ShouldBindJSON(&credential)
|
err := c.ShouldBindJSON(&credential)
|
||||||
|
|
||||||
@@ -141,6 +150,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Err
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext("")
|
||||||
|
|
||||||
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
|
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
|
||||||
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
|
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
|
||||||
@@ -151,7 +161,7 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Err
|
|||||||
applicationCloudSettingSlice = &userApplicationCloudSettings.Settings
|
applicationCloudSettingSlice = &userApplicationCloudSettings.Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof(c, "[authorizations.AuthorizeHandler] user \"uid:%d\" has logined, token type is %d, token will be expired at %d", user.Uid, claims.Type, claims.ExpiresAt)
|
log.Infof(c, "[authorizations.AuthorizeHandler] user \"uid:%d\" has logged in, token type is %d, token will be expired at %d", user.Uid, claims.Type, claims.ExpiresAt)
|
||||||
|
|
||||||
authResp := a.getAuthResponse(c, token, twoFactorEnable, user, applicationCloudSettingSlice)
|
authResp := a.getAuthResponse(c, token, twoFactorEnable, user, applicationCloudSettingSlice)
|
||||||
return authResp, nil
|
return authResp, nil
|
||||||
@@ -159,6 +169,10 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Err
|
|||||||
|
|
||||||
// TwoFactorAuthorizeHandler verifies and authorizes current 2fa login by passcode
|
// TwoFactorAuthorizeHandler verifies and authorizes current 2fa login by passcode
|
||||||
func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any, *errs.Error) {
|
func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
|
if !a.CurrentConfig().EnableInternalAuth {
|
||||||
|
return nil, errs.ErrCannotLoginByPassword
|
||||||
|
}
|
||||||
|
|
||||||
var credential models.TwoFactorLoginRequest
|
var credential models.TwoFactorLoginRequest
|
||||||
err := c.ShouldBindJSON(&credential)
|
err := c.ShouldBindJSON(&credential)
|
||||||
|
|
||||||
@@ -198,7 +212,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any,
|
|||||||
user, err := a.users.GetUserById(c, uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
log.Errorf(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,6 +242,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any,
|
|||||||
|
|
||||||
c.SetTextualToken(token)
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext("")
|
||||||
|
|
||||||
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
|
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
|
||||||
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
|
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
|
||||||
@@ -246,6 +261,10 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any,
|
|||||||
|
|
||||||
// TwoFactorAuthorizeByRecoveryCodeHandler verifies and authorizes current 2fa login by recovery code
|
// TwoFactorAuthorizeByRecoveryCodeHandler verifies and authorizes current 2fa login by recovery code
|
||||||
func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebContext) (any, *errs.Error) {
|
func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
|
if !a.CurrentConfig().EnableInternalAuth {
|
||||||
|
return nil, errs.ErrCannotLoginByPassword
|
||||||
|
}
|
||||||
|
|
||||||
var credential models.TwoFactorRecoveryCodeLoginRequest
|
var credential models.TwoFactorRecoveryCodeLoginRequest
|
||||||
err := c.ShouldBindJSON(&credential)
|
err := c.ShouldBindJSON(&credential)
|
||||||
|
|
||||||
@@ -276,7 +295,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebC
|
|||||||
user, err := a.users.GetUserById(c, uid)
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
log.Errorf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +341,7 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebC
|
|||||||
|
|
||||||
c.SetTextualToken(token)
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext("")
|
||||||
|
|
||||||
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
|
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
|
||||||
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
|
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
|
||||||
@@ -338,6 +358,184 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebC
|
|||||||
return authResp, nil
|
return authResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OAuth2CallbackAuthorizeHandler verifies and authorizes current OAuth 2.0 callback login
|
||||||
|
func (a *AuthorizationsApi) OAuth2CallbackAuthorizeHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
|
if !a.CurrentConfig().EnableOAuth2Login {
|
||||||
|
return nil, errs.ErrOAuth2NotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
var credential models.OAuth2CallbackLoginRequest
|
||||||
|
err := c.ShouldBindJSON(&credential)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] parse request failed, because %s", err.Error())
|
||||||
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenContext models.OAuth2CallbackTokenContext
|
||||||
|
err = json.Unmarshal([]byte(c.GetTokenContext()), &tokenContext)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] parse token context failed, because %s", err.Error())
|
||||||
|
return nil, errs.ErrOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tokenContext.ExternalAuthType.IsValid() {
|
||||||
|
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] external auth type \"%s\" is invalid", tokenContext.ExternalAuthType)
|
||||||
|
return nil, errs.ErrInvalidOAuth2Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
err = a.CheckFailureCount(c, uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] cannot auth for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrFailureCountLimitReached)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
|
||||||
|
return nil, errs.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Disabled {
|
||||||
|
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] user \"uid:%d\" is disabled", user.Uid)
|
||||||
|
return nil, errs.ErrUserIsDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.CurrentConfig().EnableUserForceVerifyEmail && !user.EmailVerified {
|
||||||
|
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] user \"uid:%d\" has not verified email", user.Uid)
|
||||||
|
return nil, errs.ErrEmailIsNotVerified
|
||||||
|
}
|
||||||
|
|
||||||
|
oldTokenClaims := c.GetTokenClaims()
|
||||||
|
|
||||||
|
if oldTokenClaims.Type == core.USER_TOKEN_TYPE_OAUTH2_CALLBACK_REQUIRE_VERIFY {
|
||||||
|
if credential.Password == "" {
|
||||||
|
return nil, errs.ErrPasswordIsEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.users.IsPasswordEqualsUserPassword(credential.Password, user) {
|
||||||
|
failureCheckErr := a.CheckAndIncreaseFailureCount(c, uid)
|
||||||
|
|
||||||
|
if failureCheckErr != nil {
|
||||||
|
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] cannot login for user \"uid:%d\", because %s", user.Uid, failureCheckErr.Error())
|
||||||
|
return nil, errs.Or(failureCheckErr, errs.ErrFailureCountLimitReached)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errs.ErrUserPasswordWrong
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.CurrentConfig().EnableTwoFactor {
|
||||||
|
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, errs.ErrTwoFactorIsNotEnabled) {
|
||||||
|
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to check two-factor setting for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrSystemError)
|
||||||
|
}
|
||||||
|
|
||||||
|
if twoFactorSetting != nil {
|
||||||
|
if credential.Passcode == "" {
|
||||||
|
return nil, errs.ErrPasscodeEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
if !totp.Validate(credential.Passcode, twoFactorSetting.Secret) {
|
||||||
|
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] passcode is invalid for user \"uid:%d\"", uid)
|
||||||
|
|
||||||
|
err = a.CheckAndIncreaseFailureCount(c, uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] cannot auth for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrFailureCountLimitReached)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errs.ErrPasscodeInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userExternalAuth := &models.UserExternalAuth{
|
||||||
|
Uid: user.Uid,
|
||||||
|
ExternalAuthType: tokenContext.ExternalAuthType,
|
||||||
|
ExternalUsername: tokenContext.ExternalUsername,
|
||||||
|
ExternalEmail: tokenContext.ExternalEmail,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.userExternalAuths.CreateUserExternalAuth(c, userExternalAuth)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to create user external auth for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof(c, "[authorizations.OAuth2CallbackAuthorizeHandler] user external auth has been created for user \"uid:%d\"", user.Uid)
|
||||||
|
} else if oldTokenClaims.Type == core.USER_TOKEN_TYPE_OAUTH2_CALLBACK {
|
||||||
|
_, err = a.userExternalAuths.GetUserExternalAuthByUid(c, uid, tokenContext.ExternalAuthType)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to get user external auth for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrUserExternalAuthNotFound)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, errs.ErrSystemError
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.tokens.DeleteTokenByClaims(c, oldTokenClaims)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var token string
|
||||||
|
var claims *core.UserTokenClaims
|
||||||
|
|
||||||
|
if credential.Token != "" {
|
||||||
|
_, claims, _, err = a.tokens.ParseToken(c, credential.Token)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to parse token, because %s", err.Error())
|
||||||
|
return nil, errs.ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Uid != user.Uid {
|
||||||
|
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] oauth 2.0 user \"uid:%d\" does not match current user \"uid:%d\"", user.Uid, claims.Uid)
|
||||||
|
token = ""
|
||||||
|
claims = nil
|
||||||
|
} else {
|
||||||
|
token = credential.Token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
token, claims, err = a.tokens.CreateToken(c, user)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
return nil, errs.ErrTokenGenerating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTextualToken(token)
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext("")
|
||||||
|
|
||||||
|
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
|
||||||
|
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[authorizations.OAuth2CallbackAuthorizeHandler] failed to get latest user application cloud settings for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
} else if userApplicationCloudSettings != nil && len(userApplicationCloudSettings.Settings) > 0 {
|
||||||
|
applicationCloudSettingSlice = &userApplicationCloudSettings.Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof(c, "[authorizations.OAuth2CallbackAuthorizeHandler] user \"uid:%d\" has logged in, token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
|
||||||
|
authResp := a.getAuthResponse(c, token, false, user, applicationCloudSettingSlice)
|
||||||
|
return authResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *AuthorizationsApi) getAuthResponse(c *core.WebContext, token string, need2FA bool, user *models.User, applicationCloudSettings *models.ApplicationCloudSettingSlice) *models.AuthResponse {
|
func (a *AuthorizationsApi) getAuthResponse(c *core.WebContext, token string, need2FA bool, user *models.User, applicationCloudSettings *models.ApplicationCloudSettingSlice) *models.AuthResponse {
|
||||||
return &models.AuthResponse{
|
return &models.AuthResponse{
|
||||||
Token: token,
|
Token: token,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
@@ -113,6 +114,11 @@ func (a *ApiUsingDuplicateChecker) GetSubmissionRemark(checkerType duplicatechec
|
|||||||
return a.container.GetSubmissionRemark(checkerType, uid, identification)
|
return a.container.GetSubmissionRemark(checkerType, uid, identification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSubmissionRemarkWithCustomExpiration saves the identification and remark by the current duplicate checker with custom expiration time
|
||||||
|
func (a *ApiUsingDuplicateChecker) SetSubmissionRemarkWithCustomExpiration(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string, remark string, expiration time.Duration) {
|
||||||
|
a.container.SetSubmissionRemarkWithCustomExpiration(checkerType, uid, identification, remark, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
// SetSubmissionRemarkIfEnable saves the identification and remark by the current duplicate checker if the duplicate submission check is enabled
|
// SetSubmissionRemarkIfEnable saves the identification and remark by the current duplicate checker if the duplicate submission check is enabled
|
||||||
func (a *ApiUsingDuplicateChecker) SetSubmissionRemarkIfEnable(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string, remark string) {
|
func (a *ApiUsingDuplicateChecker) SetSubmissionRemarkIfEnable(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string, remark string) {
|
||||||
if a.CurrentConfig().EnableDuplicateSubmissionsCheck {
|
if a.CurrentConfig().EnableDuplicateSubmissionsCheck {
|
||||||
@@ -120,6 +126,11 @@ func (a *ApiUsingDuplicateChecker) SetSubmissionRemarkIfEnable(checkerType dupli
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveSubmissionRemark removes the identification and remark by the current duplicate checker
|
||||||
|
func (a *ApiUsingDuplicateChecker) RemoveSubmissionRemark(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string) {
|
||||||
|
a.container.RemoveSubmissionRemark(checkerType, uid, identification)
|
||||||
|
}
|
||||||
|
|
||||||
// RemoveSubmissionRemarkIfEnable removes the identification and remark by the current duplicate checker if the duplicate submission check is enabled
|
// RemoveSubmissionRemarkIfEnable removes the identification and remark by the current duplicate checker if the duplicate submission check is enabled
|
||||||
func (a *ApiUsingDuplicateChecker) RemoveSubmissionRemarkIfEnable(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string) {
|
func (a *ApiUsingDuplicateChecker) RemoveSubmissionRemarkIfEnable(checkerType duplicatechecker.DuplicateCheckerType, uid int64, identification string) {
|
||||||
if a.CurrentConfig().EnableDuplicateSubmissionsCheck {
|
if a.CurrentConfig().EnableDuplicateSubmissionsCheck {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -383,7 +384,7 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
maxTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix())
|
maxTransactionTime := int64(math.MaxInt64)
|
||||||
minTransactionTime := int64(0)
|
minTransactionTime := int64(0)
|
||||||
|
|
||||||
if exportTransactionDataReq.MaxTime > 0 {
|
if exportTransactionDataReq.MaxTime > 0 {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
// ForgetPasswordsApi represents user forget password api
|
// ForgetPasswordsApi represents user forget password api
|
||||||
type ForgetPasswordsApi struct {
|
type ForgetPasswordsApi struct {
|
||||||
ApiUsingConfig
|
ApiUsingConfig
|
||||||
|
ApiUsingDuplicateChecker
|
||||||
users *services.UserService
|
users *services.UserService
|
||||||
tokens *services.TokenService
|
tokens *services.TokenService
|
||||||
forgetPasswords *services.ForgetPasswordService
|
forgetPasswords *services.ForgetPasswordService
|
||||||
@@ -25,6 +27,12 @@ var (
|
|||||||
ApiUsingConfig: ApiUsingConfig{
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
container: settings.Container,
|
container: settings.Container,
|
||||||
},
|
},
|
||||||
|
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
container: duplicatechecker.Container,
|
||||||
|
},
|
||||||
users: services.Users,
|
users: services.Users,
|
||||||
tokens: services.Tokens,
|
tokens: services.Tokens,
|
||||||
forgetPasswords: services.ForgetPasswords,
|
forgetPasswords: services.ForgetPasswords,
|
||||||
@@ -41,6 +49,13 @@ func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.WebContext
|
|||||||
return nil, errs.ErrEmailIsEmptyOrInvalid
|
return nil, errs.ErrEmailIsEmptyOrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = a.CheckFailureCount(c, 0)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[forget_passwords.UserForgetPasswordRequestHandler] cannot send forget password mail to \"%s\", because %s", request.Email, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrFailureCountLimitReached)
|
||||||
|
}
|
||||||
|
|
||||||
user, err := a.users.GetUserByEmail(c, request.Email)
|
user, err := a.users.GetUserByEmail(c, request.Email)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -48,6 +63,13 @@ func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.WebContext
|
|||||||
log.Errorf(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to get user, because %s", err.Error())
|
log.Errorf(c, "[forget_passwords.UserForgetPasswordRequestHandler] failed to get user, because %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
failureCheckErr := a.CheckAndIncreaseFailureCount(c, 0)
|
||||||
|
|
||||||
|
if failureCheckErr != nil {
|
||||||
|
log.Warnf(c, "[forget_passwords.UserForgetPasswordRequestHandler] cannot send forget password mail to \"%s\", because %s", request.Email, failureCheckErr.Error())
|
||||||
|
return nil, errs.Or(failureCheckErr, errs.ErrFailureCountLimitReached)
|
||||||
|
}
|
||||||
|
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +146,7 @@ func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.WebContext) (any,
|
|||||||
|
|
||||||
if user.Email != request.Email {
|
if user.Email != request.Email {
|
||||||
log.Warnf(c, "[forget_passwords.UserResetPasswordHandler] request email not equals the user email")
|
log.Warnf(c, "[forget_passwords.UserResetPasswordHandler] request email not equals the user email")
|
||||||
return nil, errs.ErrEmptyIsInvalid
|
return nil, errs.ErrEmailIsInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.users.IsPasswordEqualsUserPassword(request.Password, user) {
|
if a.users.IsPasswordEqualsUserPassword(request.Password, user) {
|
||||||
|
|||||||
@@ -0,0 +1,404 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
|
"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/utils"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||||
|
)
|
||||||
|
|
||||||
|
const oauth2CallbackPageUrlSuccessFormat = "%sdesktop/#/oauth2_callback?platform=%s&provider=%s&token=%s"
|
||||||
|
const oauth2CallbackPageUrlNeedVerifyFormat = "%sdesktop/#/oauth2_callback?platform=%s&provider=%s&userName=%s&token=%s"
|
||||||
|
const oauth2CallbackPageUrlFailedFormat = "%sdesktop/#/oauth2_callback?errorCode=%d&errorMessage=%s"
|
||||||
|
const oauth2CallbackPageUrlErrorMessageFormat = "%sdesktop/#/oauth2_callback?errorMessage=%s"
|
||||||
|
|
||||||
|
// OAuth2AuthenticationApi represents OAuth 2.0 authorization api
|
||||||
|
type OAuth2AuthenticationApi struct {
|
||||||
|
ApiUsingConfig
|
||||||
|
ApiUsingDuplicateChecker
|
||||||
|
users *services.UserService
|
||||||
|
tokens *services.TokenService
|
||||||
|
userExternalAuths *services.UserExternalAuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a OAuth 2.0 authentication api singleton instance
|
||||||
|
var (
|
||||||
|
OAuth2Authentications = &OAuth2AuthenticationApi{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
container: duplicatechecker.Container,
|
||||||
|
},
|
||||||
|
users: services.Users,
|
||||||
|
tokens: services.Tokens,
|
||||||
|
userExternalAuths: services.UserExternalAuths,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoginHandler handles user login request via OAuth 2.0
|
||||||
|
func (a *OAuth2AuthenticationApi) LoginHandler(c *core.WebContext) (string, *errs.Error) {
|
||||||
|
var oauth2LoginReq models.OAuth2LoginRequest
|
||||||
|
err := c.ShouldBindQuery(&oauth2LoginReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[oauth2_authentications.LoginHandler] parse request failed, because %s", err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.NewIncompleteOrIncorrectSubmissionError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if oauth2LoginReq.Platform != "mobile" && oauth2LoginReq.Platform != "desktop" {
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2LoginRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_OAUTH2_REDIRECT, 0, oauth2LoginReq.ClientSessionId)
|
||||||
|
|
||||||
|
if found {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.LoginHandler] another oauth 2.0 state \"%s\" has been processing for client session id \"%s\"", remark, oauth2LoginReq.ClientSessionId)
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrRepeatedRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := int64(0)
|
||||||
|
|
||||||
|
if oauth2LoginReq.Token != "" {
|
||||||
|
_, claims, _, err := a.tokens.ParseToken(c, oauth2LoginReq.Token)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.LoginHandler] failed to parse token, because %s", err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
uid = claims.Uid
|
||||||
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, errs.ErrUserNotFound) {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.LoginHandler] failed to get user by id %d, because %s", uid, err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_OAUTH2_LOGIN) {
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrNotPermittedToPerformThisAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier, err := utils.GetRandomNumberOrLowercaseLetter(64)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.LoginHandler] failed to generate random string for oauth 2.0 state, because %s", err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrSystemError)
|
||||||
|
}
|
||||||
|
|
||||||
|
remark = fmt.Sprintf("%s|%s|%d|%s", oauth2LoginReq.Platform, oauth2LoginReq.ClientSessionId, uid, verifier)
|
||||||
|
state := fmt.Sprintf("%s|%s|%s", oauth2LoginReq.Platform, oauth2LoginReq.ClientSessionId, utils.MD5EncodeToString([]byte(remark)))
|
||||||
|
|
||||||
|
redirectUrl, err := oauth2.GetOAuth2AuthUrl(c, state, verifier)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.LoginHandler] failed to get oauth 2.0 auth url, because %s", err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrSystemError))
|
||||||
|
}
|
||||||
|
|
||||||
|
a.SetSubmissionRemarkWithCustomExpiration(duplicatechecker.DUPLICATE_CHECKER_TYPE_OAUTH2_REDIRECT, 0, oauth2LoginReq.ClientSessionId, remark, a.CurrentConfig().OAuth2StateExpiredTimeDuration)
|
||||||
|
|
||||||
|
return redirectUrl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallbackHandler handles OAuth 2.0 callback request
|
||||||
|
func (a *OAuth2AuthenticationApi) CallbackHandler(c *core.WebContext) (string, *errs.Error) {
|
||||||
|
var oauth2CallbackReq models.OAuth2CallbackRequest
|
||||||
|
err := c.ShouldBindQuery(&oauth2CallbackReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[oauth2_authentications.CallbackHandler] parse request failed, because %s", err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.NewIncompleteOrIncorrectSubmissionError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if oauth2CallbackReq.State == "" {
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrMissingOAuth2State)
|
||||||
|
}
|
||||||
|
|
||||||
|
if oauth2CallbackReq.Code == "" {
|
||||||
|
if oauth2CallbackReq.ErrorDescription != "" {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] oauth 2.0 provider returned error: %s, description: %s", oauth2CallbackReq.Error, oauth2CallbackReq.ErrorDescription)
|
||||||
|
return a.redirectToErrorMessageCallbackPage(c, oauth2CallbackReq.ErrorDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrMissingOAuth2Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
platform := ""
|
||||||
|
clientSessionId := ""
|
||||||
|
|
||||||
|
stateParts := strings.Split(oauth2CallbackReq.State, "|")
|
||||||
|
|
||||||
|
if len(stateParts) == 3 {
|
||||||
|
platform = stateParts[0]
|
||||||
|
clientSessionId = stateParts[1]
|
||||||
|
} else {
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2State)
|
||||||
|
}
|
||||||
|
|
||||||
|
if platform != "mobile" && platform != "desktop" {
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2LoginRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_OAUTH2_REDIRECT, 0, clientSessionId)
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] cannot find oauth 2.0 state in duplicate checker for client session id \"%s\"", clientSessionId)
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2Callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
remarkParts := strings.Split(remark, "|")
|
||||||
|
|
||||||
|
if len(remarkParts) != 4 || remarkParts[0] != platform || remarkParts[1] != clientSessionId {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] invalid oauth 2.0 state \"%s\" in duplicate checker for client session id \"%s\"", remark, clientSessionId)
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2State)
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, err := utils.StringToInt64(remarkParts[2])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] invalid uid \"%s\" in oauth 2.0 state \"%s\"", remarkParts[2], remark)
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2State)
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier := remarkParts[3]
|
||||||
|
expectedRemark := fmt.Sprintf("%s|%s|%d|%s", platform, clientSessionId, uid, verifier)
|
||||||
|
expectedState := fmt.Sprintf("%s|%s|%s", platform, clientSessionId, utils.MD5EncodeToString([]byte(expectedRemark)))
|
||||||
|
|
||||||
|
if oauth2CallbackReq.State != expectedState {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] mismatched random string in oauth 2.0 state, expected \"%s\", got \"%s\"", expectedState, oauth2CallbackReq.State)
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrInvalidOAuth2State)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.RemoveSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_OAUTH2_REDIRECT, 0, clientSessionId)
|
||||||
|
|
||||||
|
oauth2Token, err := oauth2.GetOAuth2Token(c, oauth2CallbackReq.Code, verifier)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to retrieve oauth 2.0 token, because %s", err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrCannotRetrieveOAuth2Token))
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2UserInfo, err := oauth2.GetOAuth2UserInfo(c, oauth2Token)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to retrieve oauth 2.0 user info, because %s", err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrInvalidOAuth2Token))
|
||||||
|
}
|
||||||
|
|
||||||
|
if oauth2UserInfo == nil {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to retrieve oauth 2.0 user info, because user info is nil")
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrCannotRetrieveUserInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if oauth2UserInfo.UserName == "" || oauth2UserInfo.Email == "" {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] invalid oauth 2.0 user info, userName: %s, email: %s", oauth2UserInfo.UserName, oauth2UserInfo.Email)
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrCannotRetrieveUserInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
userExternalAuthType := oauth2.GetExternalUserAuthType()
|
||||||
|
var userExternalAuth *models.UserExternalAuth
|
||||||
|
|
||||||
|
if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierEmail {
|
||||||
|
userExternalAuth, err = a.userExternalAuths.GetUserExternalAuthByExternalEmail(c, oauth2UserInfo.Email, userExternalAuthType)
|
||||||
|
} else if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierUsername {
|
||||||
|
userExternalAuth, err = a.userExternalAuths.GetUserExternalAuthByExternalUserName(c, oauth2UserInfo.UserName, userExternalAuthType)
|
||||||
|
} else {
|
||||||
|
userExternalAuth, err = a.userExternalAuths.GetUserExternalAuthByExternalEmail(c, oauth2UserInfo.Email, userExternalAuthType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, errs.ErrUserExternalAuthNotFound) {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to get user external auth, because %s", err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
|
||||||
|
}
|
||||||
|
|
||||||
|
if uid != 0 && userExternalAuth != nil && userExternalAuth.Uid != uid {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] oauth 2.0 external auth has been bound to another user \"uid:%d\", current user \"uid:%d\"", userExternalAuth.Uid, uid)
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2UserAlreadyBoundToAnotherUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
var user *models.User
|
||||||
|
|
||||||
|
if err == nil { // user already bound to external auth, redirect to success page
|
||||||
|
user, err = a.users.GetUserById(c, userExternalAuth.Uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to get user by id %d, because %s", userExternalAuth.Uid, err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
|
||||||
|
}
|
||||||
|
} else { // errors.Is(err, errs.ErrUserExternalAuthNotFound) // user not bound to external auth, try to bind or register new user
|
||||||
|
if uid != 0 {
|
||||||
|
user, err = a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, errs.ErrUserNotFound) {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to get user by id %d, because %s", uid, err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierEmail {
|
||||||
|
user, err = a.users.GetUserByEmail(c, oauth2UserInfo.Email)
|
||||||
|
} else if a.CurrentConfig().OAuth2UserIdentifier == settings.OAuth2UserIdentifierUsername {
|
||||||
|
user, err = a.users.GetUserByUsername(c, oauth2UserInfo.UserName)
|
||||||
|
} else {
|
||||||
|
user, err = a.users.GetUserByEmail(c, oauth2UserInfo.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, errs.ErrUserNotFound) {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to get user, because %s", err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil && a.CurrentConfig().EnableUserRegister && a.CurrentConfig().OAuth2AutoRegister {
|
||||||
|
userName := strings.TrimSpace(oauth2UserInfo.UserName)
|
||||||
|
email := strings.TrimSpace(oauth2UserInfo.Email)
|
||||||
|
nickName := strings.TrimSpace(oauth2UserInfo.NickName)
|
||||||
|
languageCode := ""
|
||||||
|
currencyCode := "USD"
|
||||||
|
|
||||||
|
if nickName == "" {
|
||||||
|
nickName = userName
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.IsValidUsername(userName) {
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrUserNameIsInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.IsValidEmail(email) {
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrEmailIsInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.IsValidNickName(nickName) {
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrNickNameIsInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := locales.AllLanguages[oauth2UserInfo.LanguageCode]; exists {
|
||||||
|
languageCode = oauth2UserInfo.LanguageCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := validators.AllCurrencyNames[oauth2UserInfo.CurrencyCode]; exists {
|
||||||
|
currencyCode = oauth2UserInfo.CurrencyCode
|
||||||
|
}
|
||||||
|
|
||||||
|
user = &models.User{
|
||||||
|
Username: userName,
|
||||||
|
Email: email,
|
||||||
|
Nickname: nickName,
|
||||||
|
Language: languageCode,
|
||||||
|
DefaultCurrency: currencyCode,
|
||||||
|
FirstDayOfWeek: oauth2UserInfo.FirstDayOfWeek,
|
||||||
|
FiscalYearStart: core.FISCAL_YEAR_START_DEFAULT,
|
||||||
|
TransactionEditScope: models.TRANSACTION_EDIT_SCOPE_ALL,
|
||||||
|
FeatureRestriction: a.CurrentConfig().DefaultFeatureRestrictions,
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_OAUTH2_LOGIN) {
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrNotPermittedToPerformThisAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.users.CreateUser(c, user, true)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to create user \"%s\", because %s", user.Username, err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof(c, "[oauth2_authentications.CallbackHandler] user \"%s\" has registered successfully, uid is %d", user.Username, user.Uid)
|
||||||
|
|
||||||
|
userExternalAuth = &models.UserExternalAuth{
|
||||||
|
Uid: user.Uid,
|
||||||
|
ExternalAuthType: userExternalAuthType,
|
||||||
|
ExternalUsername: oauth2UserInfo.UserName,
|
||||||
|
ExternalEmail: oauth2UserInfo.Email,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.userExternalAuths.CreateUserExternalAuth(c, userExternalAuth)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to create user external auth for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.Or(err, errs.ErrOperationFailed))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof(c, "[oauth2_authentications.CallbackHandler] user external auth has been created for user \"uid:%d\"", user.Uid)
|
||||||
|
} else if user == nil {
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrOAuth2AutoRegistrationNotEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_OAUTH2_LOGIN) {
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrNotPermittedToPerformThisAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
if userExternalAuth == nil {
|
||||||
|
tokenContext, err := json.Marshal(&models.OAuth2CallbackTokenContext{
|
||||||
|
ExternalAuthType: userExternalAuthType,
|
||||||
|
ExternalUsername: oauth2UserInfo.UserName,
|
||||||
|
ExternalEmail: oauth2UserInfo.Email,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to marshal oauth 2.0 callback verify token context, because %s", err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, _, err := a.tokens.CreateOAuth2CallbackRequireVerifyToken(c, user, string(tokenContext))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to create oauth 2.0 callback verify token, because %s", err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrTokenGenerating)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.redirectToVerifyCallbackPage(c, platform, userExternalAuthType, user.Username, token)
|
||||||
|
} else {
|
||||||
|
tokenContext, err := json.Marshal(&models.OAuth2CallbackTokenContext{
|
||||||
|
ExternalAuthType: userExternalAuthType,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to marshal oauth 2.0 callback token context, because %s", err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, _, err := a.tokens.CreateOAuth2CallbackToken(c, user, string(tokenContext))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oauth2_authentications.CallbackHandler] failed to create oauth 2.0 callback token, because %s", err.Error())
|
||||||
|
return a.redirectToFailedCallbackPage(c, errs.ErrTokenGenerating)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.redirectToSuccessCallbackPage(c, platform, userExternalAuthType, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *OAuth2AuthenticationApi) redirectToSuccessCallbackPage(c *core.WebContext, platform string, externalAuthType core.UserExternalAuthType, token string) (string, *errs.Error) {
|
||||||
|
return fmt.Sprintf(oauth2CallbackPageUrlSuccessFormat, a.CurrentConfig().RootUrl, platform, externalAuthType, url.QueryEscape(token)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *OAuth2AuthenticationApi) redirectToVerifyCallbackPage(c *core.WebContext, platform string, externalAuthType core.UserExternalAuthType, userName string, token string) (string, *errs.Error) {
|
||||||
|
return fmt.Sprintf(oauth2CallbackPageUrlNeedVerifyFormat, a.CurrentConfig().RootUrl, platform, externalAuthType, userName, url.QueryEscape(token)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *OAuth2AuthenticationApi) redirectToFailedCallbackPage(c *core.WebContext, err *errs.Error) (string, *errs.Error) {
|
||||||
|
return fmt.Sprintf(oauth2CallbackPageUrlFailedFormat, a.CurrentConfig().RootUrl, err.Code(), url.QueryEscape(utils.GetDisplayErrorMessage(err))), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *OAuth2AuthenticationApi) redirectToErrorMessageCallbackPage(c *core.WebContext, message string) (string, *errs.Error) {
|
||||||
|
return fmt.Sprintf(oauth2CallbackPageUrlErrorMessageFormat, a.CurrentConfig().RootUrl, url.QueryEscape(message)), nil
|
||||||
|
}
|
||||||
@@ -35,14 +35,23 @@ func (a *ServerSettingsApi) ServerSettingsJavascriptHandler(c *core.WebContext)
|
|||||||
builder := &strings.Builder{}
|
builder := &strings.Builder{}
|
||||||
builder.WriteString(ezbookkeepingServerSettingsJavascriptFileHeader)
|
builder.WriteString(ezbookkeepingServerSettingsJavascriptFileHeader)
|
||||||
|
|
||||||
a.appendBooleanSetting(builder, "r", config.EnableUserRegister)
|
a.appendBooleanSetting(builder, "a", config.EnableInternalAuth)
|
||||||
a.appendBooleanSetting(builder, "f", config.EnableUserForgetPassword)
|
a.appendBooleanSetting(builder, "o", config.EnableOAuth2Login)
|
||||||
|
a.appendBooleanSetting(builder, "r", config.EnableInternalAuth && config.EnableUserRegister)
|
||||||
|
a.appendBooleanSetting(builder, "f", config.EnableInternalAuth && config.EnableUserForgetPassword)
|
||||||
|
a.appendBooleanSetting(builder, "t", config.EnableAPIToken)
|
||||||
a.appendBooleanSetting(builder, "v", config.EnableUserVerifyEmail)
|
a.appendBooleanSetting(builder, "v", config.EnableUserVerifyEmail)
|
||||||
a.appendBooleanSetting(builder, "p", config.EnableTransactionPictures)
|
a.appendBooleanSetting(builder, "p", config.EnableTransactionPictures)
|
||||||
a.appendBooleanSetting(builder, "s", config.EnableScheduledTransaction)
|
a.appendBooleanSetting(builder, "s", config.EnableScheduledTransaction)
|
||||||
a.appendBooleanSetting(builder, "e", config.EnableDataExport)
|
a.appendBooleanSetting(builder, "e", config.EnableDataExport)
|
||||||
a.appendBooleanSetting(builder, "i", config.EnableDataImport)
|
a.appendBooleanSetting(builder, "i", config.EnableDataImport)
|
||||||
|
|
||||||
|
a.appendStringSetting(builder, "op", config.OAuth2Provider)
|
||||||
|
|
||||||
|
if config.OAuth2Provider == settings.OAuth2ProviderOIDC && config.OAuth2OIDCCustomDisplayNameConfig.Enabled {
|
||||||
|
a.appendMultiLanguageTipSetting(builder, "ocn", config.OAuth2OIDCCustomDisplayNameConfig)
|
||||||
|
}
|
||||||
|
|
||||||
if config.EnableMCPServer {
|
if config.EnableMCPServer {
|
||||||
a.appendBooleanSetting(builder, "mcp", config.EnableMCPServer)
|
a.appendBooleanSetting(builder, "mcp", config.EnableMCPServer)
|
||||||
}
|
}
|
||||||
@@ -138,7 +147,7 @@ func (a *ServerSettingsApi) appendStringSetting(builder *strings.Builder, key st
|
|||||||
builder.WriteString(";\n")
|
builder.WriteString(";\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ServerSettingsApi) appendMultiLanguageTipSetting(builder *strings.Builder, key string, value settings.TipConfig) {
|
func (a *ServerSettingsApi) appendMultiLanguageTipSetting(builder *strings.Builder, key string, value settings.MultiLanguageContentConfig) {
|
||||||
builder.WriteString(ezbookkeepingServerSettingsGlobalVariableFullName)
|
builder.WriteString(ezbookkeepingServerSettingsGlobalVariableFullName)
|
||||||
builder.WriteString("[")
|
builder.WriteString("[")
|
||||||
a.appendEncodedString(builder, key)
|
a.appendEncodedString(builder, key)
|
||||||
|
|||||||
+53
-3
@@ -69,7 +69,9 @@ func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
|
|||||||
tokenResp.IsCurrent = true
|
tokenResp.IsCurrent = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if token.TokenType == core.USER_TOKEN_TYPE_MCP && token.UserAgent != services.TokenUserAgentCreatedViaCli {
|
if token.TokenType == core.USER_TOKEN_TYPE_API && token.UserAgent != services.TokenUserAgentCreatedViaCli {
|
||||||
|
tokenResp.UserAgent = services.TokenUserAgentForAPI
|
||||||
|
} else if token.TokenType == core.USER_TOKEN_TYPE_MCP && token.UserAgent != services.TokenUserAgentCreatedViaCli {
|
||||||
tokenResp.UserAgent = services.TokenUserAgentForMCP
|
tokenResp.UserAgent = services.TokenUserAgentForMCP
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +83,53 @@ func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
|
|||||||
return tokenResps, nil
|
return tokenResps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TokenGenerateAPIHandler generates a new API token for current user
|
||||||
|
func (a *TokensApi) TokenGenerateAPIHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
|
if !a.CurrentConfig().EnableAPIToken {
|
||||||
|
return nil, errs.ErrAPITokenNotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
var generateAPITokenReq models.TokenGenerateAPIRequest
|
||||||
|
err := c.ShouldBindJSON(&generateAPITokenReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[tokens.TokenGenerateAPIHandler] parse request failed, because %s", err.Error())
|
||||||
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[tokens.TokenGenerateAPIHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
|
||||||
|
return nil, errs.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_GENERATE_API_TOKEN) {
|
||||||
|
return false, errs.ErrNotPermittedToPerformThisAction
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.users.IsPasswordEqualsUserPassword(generateAPITokenReq.Password, user) {
|
||||||
|
return nil, errs.ErrUserPasswordWrong
|
||||||
|
}
|
||||||
|
|
||||||
|
token, claims, err := a.tokens.CreateAPIToken(c, user, generateAPITokenReq.ExpiredInSeconds)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[tokens.TokenGenerateAPIHandler] failed to create api token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrTokenGenerating)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof(c, "[tokens.TokenGenerateAPIHandler] user \"uid:%d\" has generated api token, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
|
||||||
|
generateAPITokenResp := &models.TokenGenerateAPIResponse{
|
||||||
|
Token: token,
|
||||||
|
APIBaseUrl: a.CurrentConfig().RootUrl + "api",
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateAPITokenResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
// TokenGenerateMCPHandler generates a new MCP token for current user
|
// TokenGenerateMCPHandler generates a new MCP token for current user
|
||||||
func (a *TokensApi) TokenGenerateMCPHandler(c *core.WebContext) (any, *errs.Error) {
|
func (a *TokensApi) TokenGenerateMCPHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
if !a.CurrentConfig().EnableMCPServer {
|
if !a.CurrentConfig().EnableMCPServer {
|
||||||
@@ -111,7 +160,7 @@ func (a *TokensApi) TokenGenerateMCPHandler(c *core.WebContext) (any, *errs.Erro
|
|||||||
return nil, errs.ErrUserPasswordWrong
|
return nil, errs.ErrUserPasswordWrong
|
||||||
}
|
}
|
||||||
|
|
||||||
token, claims, err := a.tokens.CreateMCPToken(c, user)
|
token, claims, err := a.tokens.CreateMCPToken(c, user, generateMCPTokenReq.ExpiredInSeconds)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(c, "[tokens.TokenGenerateMCPHandler] failed to create mcp token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
log.Errorf(c, "[tokens.TokenGenerateMCPHandler] failed to create mcp token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
@@ -136,7 +185,7 @@ func (a *TokensApi) TokenRevokeCurrentHandler(c *core.WebContext) (any, *errs.Er
|
|||||||
return false, errs.ErrTokenIsEmpty
|
return false, errs.ErrTokenIsEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
_, claims, err := a.tokens.ParseToken(c, tokenString)
|
_, claims, _, err := a.tokens.ParseToken(c, tokenString)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.Or(err, errs.NewIncompleteOrIncorrectSubmissionError(err))
|
return nil, errs.Or(err, errs.NewIncompleteOrIncorrectSubmissionError(err))
|
||||||
@@ -344,6 +393,7 @@ func (a *TokensApi) TokenRefreshHandler(c *core.WebContext) (any, *errs.Error) {
|
|||||||
|
|
||||||
c.SetTextualToken(token)
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext("")
|
||||||
|
|
||||||
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
|
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
|
||||||
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
|
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
|
||||||
|
|||||||
+135
-3
@@ -340,7 +340,7 @@ func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebC
|
|||||||
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(reconciliationStatementRequest.StartTime)
|
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(reconciliationStatementRequest.StartTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionsWithAccountBalance, totalInflows, totalOutflows, openingBalance, closingBalance, err := a.transactions.GetAllTransactionsWithAccountBalanceByMaxTime(c, uid, pageCountForAccountStatement, maxTransactionTime, minTransactionTime, reconciliationStatementRequest.AccountId, account.Category)
|
transactionsWithAccountBalance, totalInflows, totalOutflows, openingBalance, closingBalance, err := a.transactions.GetAllTransactionsInOneAccountWithAccountBalanceByMaxTime(c, uid, pageCountForAccountStatement, maxTransactionTime, minTransactionTime, reconciliationStatementRequest.AccountId, account.Category)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get transactions from \"%d\" to \"%d\" for user \"uid:%d\", because %s", reconciliationStatementRequest.StartTime, reconciliationStatementRequest.EndTime, uid, err.Error())
|
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get transactions from \"%d\" to \"%d\" for user \"uid:%d\", because %s", reconciliationStatementRequest.StartTime, reconciliationStatementRequest.EndTime, uid, err.Error())
|
||||||
@@ -426,7 +426,7 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.WebContext) (any,
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalIncomeAndExpense(c, uid, statisticReq.StartTime, statisticReq.EndTime, allTagIds, noTags, statisticReq.TagFilterType, statisticReq.Keyword, utcOffset, statisticReq.UseTransactionTimezone)
|
totalAmounts, err := a.transactions.GetAccountsAndCategoriesTotalInflowAndOutflow(c, uid, statisticReq.StartTime, statisticReq.EndTime, allTagIds, noTags, statisticReq.TagFilterType, statisticReq.Keyword, utcOffset, statisticReq.UseTransactionTimezone)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(c, "[transactions.TransactionStatisticsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[transactions.TransactionStatisticsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
@@ -447,6 +447,11 @@ func (a *TransactionsApi) TransactionStatisticsHandler(c *core.WebContext) (any,
|
|||||||
AccountId: totalAmountItem.AccountId,
|
AccountId: totalAmountItem.AccountId,
|
||||||
TotalAmount: totalAmountItem.Amount,
|
TotalAmount: totalAmountItem.Amount,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||||
|
statisticResp.Items[i].RelatedAccountId = totalAmountItem.RelatedAccountId
|
||||||
|
statisticResp.Items[i].RelatedAccountType, _ = totalAmountItem.Type.ToTransactionRelatedAccountType()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return statisticResp, nil
|
return statisticResp, nil
|
||||||
@@ -489,7 +494,7 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext)
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyIncomeAndExpense(c, uid, startYear, startMonth, endYear, endMonth, allTagIds, noTags, statisticTrendsReq.TagFilterType, statisticTrendsReq.Keyword, utcOffset, statisticTrendsReq.UseTransactionTimezone)
|
allMonthlyTotalAmounts, err := a.transactions.GetAccountsAndCategoriesMonthlyInflowAndOutflow(c, uid, startYear, startMonth, endYear, endMonth, allTagIds, noTags, statisticTrendsReq.TagFilterType, statisticTrendsReq.Keyword, utcOffset, statisticTrendsReq.UseTransactionTimezone)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(c, "[transactions.TransactionStatisticsTrendsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[transactions.TransactionStatisticsTrendsHandler] failed to get accounts and categories total income and expense for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
@@ -512,6 +517,11 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext)
|
|||||||
AccountId: totalAmountItem.AccountId,
|
AccountId: totalAmountItem.AccountId,
|
||||||
TotalAmount: totalAmountItem.Amount,
|
TotalAmount: totalAmountItem.Amount,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || totalAmountItem.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||||
|
monthlyStatisticResp.Items[i].RelatedAccountId = totalAmountItem.RelatedAccountId
|
||||||
|
monthlyStatisticResp.Items[i].RelatedAccountType, _ = totalAmountItem.Type.ToTransactionRelatedAccountType()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statisticTrendsResp = append(statisticTrendsResp, monthlyStatisticResp)
|
statisticTrendsResp = append(statisticTrendsResp, monthlyStatisticResp)
|
||||||
@@ -522,6 +532,71 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext)
|
|||||||
return statisticTrendsResp, nil
|
return statisticTrendsResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TransactionStatisticsAssetTrendsHandler returns transaction statistics asset trends of current user
|
||||||
|
func (a *TransactionsApi) TransactionStatisticsAssetTrendsHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
|
var statisticAssetTrendsReq models.TransactionStatisticAssetTrendsRequest
|
||||||
|
err := c.ShouldBindQuery(&statisticAssetTrendsReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] parse request failed, because %s", err.Error())
|
||||||
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
utcOffset, err := c.GetClientTimezoneOffset()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] cannot get client timezone offset, because %s", err.Error())
|
||||||
|
return nil, errs.ErrClientTimezoneOffsetInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
|
||||||
|
maxTransactionTime := int64(0)
|
||||||
|
|
||||||
|
if statisticAssetTrendsReq.EndTime > 0 {
|
||||||
|
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(statisticAssetTrendsReq.EndTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
minTransactionTime := int64(0)
|
||||||
|
|
||||||
|
if statisticAssetTrendsReq.StartTime > 0 {
|
||||||
|
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(statisticAssetTrendsReq.StartTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountDailyBalances, err := a.transactions.GetAllAccountsDailyOpeningAndClosingBalance(c, uid, maxTransactionTime, minTransactionTime, utcOffset)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] failed to get transactions from \"%d\" to \"%d\" for user \"uid:%d\", because %s", statisticAssetTrendsReq.StartTime, statisticAssetTrendsReq.EndTime, uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
statisticAssetTrendsResp := make(models.TransactionStatisticAssetTrendsResponseItemSlice, 0)
|
||||||
|
|
||||||
|
for yearMonthDay, dailyAccountBalances := range accountDailyBalances {
|
||||||
|
dailyStatisticResp := &models.TransactionStatisticAssetTrendsResponseItem{
|
||||||
|
Year: yearMonthDay / 10000,
|
||||||
|
Month: (yearMonthDay % 10000) / 100,
|
||||||
|
Day: yearMonthDay % 100,
|
||||||
|
Items: make([]*models.TransactionStatisticAssetTrendsResponseDataItem, len(dailyAccountBalances)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(dailyAccountBalances); i++ {
|
||||||
|
accountBalance := dailyAccountBalances[i]
|
||||||
|
dailyStatisticResp.Items[i] = &models.TransactionStatisticAssetTrendsResponseDataItem{
|
||||||
|
AccountId: accountBalance.AccountId,
|
||||||
|
AccountOpeningBalance: accountBalance.AccountOpeningBalance,
|
||||||
|
AccountClosingBalance: accountBalance.AccountClosingBalance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statisticAssetTrendsResp = append(statisticAssetTrendsResp, dailyStatisticResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(statisticAssetTrendsResp)
|
||||||
|
|
||||||
|
return statisticAssetTrendsResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
// TransactionAmountsHandler returns transaction amounts of current user
|
// TransactionAmountsHandler returns transaction amounts of current user
|
||||||
func (a *TransactionsApi) TransactionAmountsHandler(c *core.WebContext) (any, *errs.Error) {
|
func (a *TransactionsApi) TransactionAmountsHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var transactionAmountsReq models.TransactionAmountsRequest
|
var transactionAmountsReq models.TransactionAmountsRequest
|
||||||
@@ -1114,6 +1189,63 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er
|
|||||||
return newTransactionResp, nil
|
return newTransactionResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TransactionMoveAllBetweenAccountsHandler moves all transactions from one account to another account for current user
|
||||||
|
func (a *TransactionsApi) TransactionMoveAllBetweenAccountsHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
|
var transactionMoveReq models.TransactionMoveBetweenAccountsRequest
|
||||||
|
err := c.ShouldBindJSON(&transactionMoveReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[transactions.TransactionMoveAllBetweenAccountsHandler] parse request failed, because %s", err.Error())
|
||||||
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if transactionMoveReq.FromAccountId == transactionMoveReq.ToAccountId {
|
||||||
|
return nil, errs.ErrCannotMoveTransactionToSameAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
accountMap, err := a.accounts.GetAccountsByAccountIds(c, uid, []int64{transactionMoveReq.FromAccountId, transactionMoveReq.ToAccountId})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.TransactionMoveAllBetweenAccountsHandler] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fromAccount, exists := accountMap[transactionMoveReq.FromAccountId]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, errs.ErrSourceAccountNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
toAccount, exists := accountMap[transactionMoveReq.ToAccountId]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, errs.ErrDestinationAccountNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if fromAccount.Hidden || toAccount.Hidden {
|
||||||
|
return nil, errs.ErrCannotMoveTransactionFromOrToHiddenAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
if fromAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS || toAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS {
|
||||||
|
return nil, errs.ErrCannotMoveTransactionFromOrToParentAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
if fromAccount.Currency != toAccount.Currency {
|
||||||
|
return nil, errs.ErrCannotMoveTransactionBetweenAccountsWithDifferentCurrencies
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.transactions.MoveAllTransactionsBetweenAccounts(c, uid, transactionMoveReq.FromAccountId, transactionMoveReq.ToAccountId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.TransactionMoveAllBetweenAccountsHandler] failed to move all transactions from account \"id:%d\" to account \"id:%d\" for user \"uid:%d\", because %s", transactionMoveReq.FromAccountId, transactionMoveReq.ToAccountId, uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof(c, "[transactions.TransactionMoveAllBetweenAccountsHandler] user \"uid:%d\" has moved all transactions from account \"id:%d\" to account \"id:%d\" successfully", uid, transactionMoveReq.FromAccountId, transactionMoveReq.ToAccountId)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// TransactionDeleteHandler deletes an existed transaction by request parameters for current user
|
// TransactionDeleteHandler deletes an existed transaction by request parameters for current user
|
||||||
func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *errs.Error) {
|
func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
var transactionDeleteReq models.TransactionDeleteRequest
|
var transactionDeleteReq models.TransactionDeleteRequest
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.WebCo
|
|||||||
|
|
||||||
c.SetTextualToken(token)
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext("")
|
||||||
|
|
||||||
log.Infof(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
log.Infof(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserExternalAuthsApi represents user external auth api
|
||||||
|
type UserExternalAuthsApi struct {
|
||||||
|
users *services.UserService
|
||||||
|
userExternalAuths *services.UserExternalAuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a user external auth api singleton instance
|
||||||
|
var (
|
||||||
|
UserExternalAuths = &UserExternalAuthsApi{
|
||||||
|
users: services.Users,
|
||||||
|
userExternalAuths: services.UserExternalAuths,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExternalAuthListHanlder returns external authentications list of current user
|
||||||
|
func (a *UserExternalAuthsApi) ExternalAuthListHanlder(c *core.WebContext) (any, *errs.Error) {
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
userExternalAuths, err := a.userExternalAuths.GetUserAllExternalAuthsByUid(c, uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[user_external_auths.ExternalAuthListHanlder] failed to get all external authentications for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
userExternalAuthResps := make(models.UserExternalAuthInfoResponsesSlice, 0, len(userExternalAuths)+1)
|
||||||
|
currentExternalAuthType := oauth2.GetExternalUserAuthType()
|
||||||
|
hasCurrentExternalAuth := false
|
||||||
|
|
||||||
|
for i := 0; i < len(userExternalAuths); i++ {
|
||||||
|
userExternalAuth := userExternalAuths[i]
|
||||||
|
|
||||||
|
if userExternalAuth.ExternalAuthType == currentExternalAuthType {
|
||||||
|
hasCurrentExternalAuth = true
|
||||||
|
}
|
||||||
|
|
||||||
|
userExternalAuthResps = append(userExternalAuthResps, userExternalAuth.ToUserExternalAuthInfoResponse())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasCurrentExternalAuth {
|
||||||
|
userExternalAuthResps = append(userExternalAuthResps, &models.UserExternalAuthInfoResponse{
|
||||||
|
ExternalAuthCategory: currentExternalAuthType.GetCategory(),
|
||||||
|
ExternalAuthType: currentExternalAuthType,
|
||||||
|
Linked: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(userExternalAuthResps)
|
||||||
|
|
||||||
|
return userExternalAuthResps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnlinkExternalAuthHandler unlinks external authentication for current user
|
||||||
|
func (a *UserExternalAuthsApi) UnlinkExternalAuthHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
|
var externalAuthLinkReq models.UserExternalAuthUnlinkRequest
|
||||||
|
err := c.ShouldBindJSON(&externalAuthLinkReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[user_external_auths.UnlinkExternalAuthHandler] parse request failed, because %s", err.Error())
|
||||||
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
user, err := a.users.GetUserById(c, uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if !errs.IsCustomError(err) {
|
||||||
|
log.Warnf(c, "[user_external_auths.UnlinkExternalAuthHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errs.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.users.IsPasswordEqualsUserPassword(externalAuthLinkReq.Password, user) {
|
||||||
|
return nil, errs.ErrUserPasswordWrong
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_UNLINK_THIRD_PARTY_LOGIN) {
|
||||||
|
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||||
|
}
|
||||||
|
|
||||||
|
externalAuthType := core.UserExternalAuthType(externalAuthLinkReq.ExternalAuthType)
|
||||||
|
|
||||||
|
if !externalAuthType.IsValid() {
|
||||||
|
return nil, errs.ErrUserExternalAuthNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.userExternalAuths.DeleteUserExternalAuth(c, uid, externalAuthType)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[user_external_auths.UnlinkExternalAuthHandler] failed to unlink external authentication \"%s\" for user \"uid:%d\", because %s", externalAuthLinkReq.ExternalAuthType, uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
+6
-3
@@ -83,7 +83,7 @@ func (a *UsersApi) UserRegisterHandler(c *core.WebContext) (any, *errs.Error) {
|
|||||||
FeatureRestriction: a.CurrentConfig().DefaultFeatureRestrictions,
|
FeatureRestriction: a.CurrentConfig().DefaultFeatureRestrictions,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.users.CreateUser(c, user)
|
err = a.users.CreateUser(c, user, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(c, "[users.UserRegisterHandler] failed to create user \"%s\", because %s", user.Username, err.Error())
|
log.Errorf(c, "[users.UserRegisterHandler] failed to create user \"%s\", because %s", user.Username, err.Error())
|
||||||
@@ -142,8 +142,9 @@ func (a *UsersApi) UserRegisterHandler(c *core.WebContext) (any, *errs.Error) {
|
|||||||
authResp.Token = token
|
authResp.Token = token
|
||||||
c.SetTextualToken(token)
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext("")
|
||||||
|
|
||||||
log.Infof(c, "[users.UserRegisterHandler] user \"uid:%d\" has logined, token will be expired at %d", user.Uid, claims.ExpiresAt)
|
log.Infof(c, "[users.UserRegisterHandler] user \"uid:%d\" has logged in, token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
|
||||||
return authResp, nil
|
return authResp, nil
|
||||||
}
|
}
|
||||||
@@ -205,6 +206,7 @@ func (a *UsersApi) UserEmailVerifyHandler(c *core.WebContext) (any, *errs.Error)
|
|||||||
|
|
||||||
c.SetTextualToken(token)
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext("")
|
||||||
|
|
||||||
log.Infof(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" token created, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
log.Infof(c, "[users.UserEmailVerifyHandler] user \"uid:%d\" token created, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
}
|
}
|
||||||
@@ -275,7 +277,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
|
|||||||
return nil, errs.ErrNotPermittedToPerformThisAction
|
return nil, errs.ErrNotPermittedToPerformThisAction
|
||||||
}
|
}
|
||||||
|
|
||||||
if !a.users.IsPasswordEqualsUserPassword(userUpdateReq.OldPassword, user) {
|
if user.Password != "" && !a.users.IsPasswordEqualsUserPassword(userUpdateReq.OldPassword, user) {
|
||||||
return nil, errs.ErrUserPasswordWrong
|
return nil, errs.ErrUserPasswordWrong
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,6 +590,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
|
|||||||
resp.NewToken = token
|
resp.NewToken = token
|
||||||
c.SetTextualToken(token)
|
c.SetTextualToken(token)
|
||||||
c.SetTokenClaims(claims)
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext("")
|
||||||
|
|
||||||
log.Infof(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
log.Infof(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import "github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
|
||||||
|
// OAuth2UserInfo represents the user info retrieved from OAuth 2.0 provider
|
||||||
|
type OAuth2UserInfo struct {
|
||||||
|
UserName string
|
||||||
|
Email string
|
||||||
|
NickName string
|
||||||
|
LanguageCode string
|
||||||
|
CurrencyCode string
|
||||||
|
FirstDayOfWeek core.WeekDay
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package oauth2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/gitea"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/github"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/nextcloud"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/oidc"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OAuth2Container contains the current OAuth 2.0 authentication provider
|
||||||
|
type OAuth2Container struct {
|
||||||
|
current provider.OAuth2Provider
|
||||||
|
usePKCE bool
|
||||||
|
oauth2HttpClient *http.Client
|
||||||
|
externalUserAuthType core.UserExternalAuthType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a OAuth 2.0 container singleton instance
|
||||||
|
var (
|
||||||
|
Container = &OAuth2Container{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitializeOAuth2Provider initializes the current OAuth 2.0 provider according to the config
|
||||||
|
func InitializeOAuth2Provider(config *settings.Config) error {
|
||||||
|
if !config.EnableOAuth2Login {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.OAuth2ClientID == "" || config.OAuth2ClientSecret == "" || config.OAuth2UserIdentifier == "" || config.OAuth2Provider == "" {
|
||||||
|
return errs.ErrInvalidOAuth2Config
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var oauth2Provider provider.OAuth2Provider
|
||||||
|
var externalUserAuthType core.UserExternalAuthType
|
||||||
|
redirectUrl := config.RootUrl + "oauth2/callback"
|
||||||
|
|
||||||
|
if config.OAuth2Provider == settings.OAuth2ProviderOIDC {
|
||||||
|
oauth2Provider, err = oidc.NewOIDCProvider(config, redirectUrl)
|
||||||
|
externalUserAuthType = core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_OIDC
|
||||||
|
} else if config.OAuth2Provider == settings.OAuth2ProviderNextcloud {
|
||||||
|
oauth2Provider, err = nextcloud.NewNextcloudOAuth2Provider(config, redirectUrl)
|
||||||
|
externalUserAuthType = core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_NEXTCLOUD
|
||||||
|
} else if config.OAuth2Provider == settings.OAuth2ProviderGitea {
|
||||||
|
oauth2Provider, err = gitea.NewGiteaOAuth2Provider(config, redirectUrl)
|
||||||
|
externalUserAuthType = core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITEA
|
||||||
|
} else if config.OAuth2Provider == settings.OAuth2ProviderGithub {
|
||||||
|
oauth2Provider, err = github.NewGithubOAuth2Provider(config, redirectUrl)
|
||||||
|
externalUserAuthType = core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITHUB
|
||||||
|
} else {
|
||||||
|
return errs.ErrInvalidOAuth2Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
Container.current = oauth2Provider
|
||||||
|
Container.usePKCE = config.OAuth2UsePKCE
|
||||||
|
Container.oauth2HttpClient = utils.NewHttpClient(config.OAuth2RequestTimeout, config.OAuth2Proxy, config.OAuth2SkipTLSVerify, settings.GetUserAgent())
|
||||||
|
Container.externalUserAuthType = externalUserAuthType
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOAuth2AuthUrl returns the OAuth 2.0 authentication url
|
||||||
|
func GetOAuth2AuthUrl(c core.Context, state string, verifier string) (string, error) {
|
||||||
|
if Container.current == nil {
|
||||||
|
return "", errs.ErrOAuth2NotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts []oauth2.AuthCodeOption
|
||||||
|
|
||||||
|
if Container.usePKCE {
|
||||||
|
opts = append(opts, oauth2.S256ChallengeOption(verifier))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container.current.GetOAuth2AuthUrl(wrapOAuth2Context(c, Container.oauth2HttpClient), state, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOAuth2Token exchanges the authorization code for an OAuth 2.0 token
|
||||||
|
func GetOAuth2Token(c core.Context, code string, verifier string) (*oauth2.Token, error) {
|
||||||
|
if Container.current == nil || Container.oauth2HttpClient == nil {
|
||||||
|
return nil, errs.ErrOAuth2NotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts []oauth2.AuthCodeOption
|
||||||
|
|
||||||
|
if Container.usePKCE {
|
||||||
|
opts = append(opts, oauth2.VerifierOption(verifier))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container.current.GetOAuth2Token(wrapOAuth2Context(c, Container.oauth2HttpClient), code, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOAuth2UserInfo retrieves the OAuth 2.0 user info using the provided OAuth 2.0 token
|
||||||
|
func GetOAuth2UserInfo(c core.Context, token *oauth2.Token) (*data.OAuth2UserInfo, error) {
|
||||||
|
if Container.current == nil || Container.oauth2HttpClient == nil {
|
||||||
|
return nil, errs.ErrOAuth2NotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if token == nil {
|
||||||
|
return nil, errs.ErrInvalidOAuth2Token
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container.current.GetUserInfo(wrapOAuth2Context(c, Container.oauth2HttpClient), token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExternalUserAuthType returns the external user auth type of the current OAuth 2.0 provider
|
||||||
|
func GetExternalUserAuthType() core.UserExternalAuthType {
|
||||||
|
return Container.externalUserAuthType
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package oauth2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OAuth2Context represents the context for OAuth 2.0 operations
|
||||||
|
type OAuth2Context struct {
|
||||||
|
core.Context
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns the value associated with key
|
||||||
|
func (c *OAuth2Context) Value(key any) any {
|
||||||
|
if key == oauth2.HTTPClient {
|
||||||
|
return c.httpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Context.Value(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapOAuth2Context(ctx core.Context, httpClient *http.Client) core.Context {
|
||||||
|
return &OAuth2Context{
|
||||||
|
Context: ctx,
|
||||||
|
httpClient: httpClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommonOAuth2Provider represents common OAuth 2.0 provider
|
||||||
|
type CommonOAuth2Provider struct {
|
||||||
|
provider.OAuth2Provider
|
||||||
|
oauth2Config *oauth2.Config
|
||||||
|
dataSource CommonOAuth2DataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommonOAuth2DataSource defines the structure of OAuth 2.0 data source
|
||||||
|
type CommonOAuth2DataSource interface {
|
||||||
|
// GetAuthUrl returns the authentication url of the data source
|
||||||
|
GetAuthUrl() string
|
||||||
|
|
||||||
|
// GetTokenUrl returns the token url of the data source
|
||||||
|
GetTokenUrl() string
|
||||||
|
|
||||||
|
// GetUserInfoRequest returns the user info request of the data source
|
||||||
|
GetUserInfoRequest() (*http.Request, error)
|
||||||
|
|
||||||
|
// GetScopes returns the scopes required by the data source
|
||||||
|
GetScopes() []string
|
||||||
|
|
||||||
|
// ParseUserInfo returns the user info by parsing the response body
|
||||||
|
ParseUserInfo(c core.Context, body []byte) (*data.OAuth2UserInfo, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOAuth2AuthUrl returns the authentication url of the common OAuth 2.0 provider
|
||||||
|
func (p *CommonOAuth2Provider) GetOAuth2AuthUrl(c core.Context, state string, opts ...oauth2.AuthCodeOption) (string, error) {
|
||||||
|
return p.oauth2Config.AuthCodeURL(state, opts...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOAuth2Token returns the OAuth 2.0 token of the common OAuth 2.0 provider
|
||||||
|
func (p *CommonOAuth2Provider) GetOAuth2Token(c core.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||||
|
return p.oauth2Config.Exchange(c, code, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo returns the user info by the common OAuth 2.0 provider
|
||||||
|
func (p *CommonOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.Token) (*data.OAuth2UserInfo, error) {
|
||||||
|
req, err := p.dataSource.GetUserInfoRequest()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[common_oauth2_provider.GetUserInfo] failed to get user info request, because %s", err.Error())
|
||||||
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2Client := oauth2.NewClient(c, oauth2.StaticTokenSource(oauth2Token))
|
||||||
|
resp, err := oauth2Client.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[common_oauth2_provider.GetUserInfo] failed to get user info response, because %s", err.Error())
|
||||||
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
log.Debugf(c, "[common_oauth2_provider.GetUserInfo] response is %s", body)
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
log.Errorf(c, "[common_oauth2_provider.GetUserInfo] failed to get user info response, because response code is %d", resp.StatusCode)
|
||||||
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.dataSource.ParseUserInfo(c, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDataSource returns the data source of the common OAuth 2.0 provider
|
||||||
|
func (p *CommonOAuth2Provider) GetDataSource() CommonOAuth2DataSource {
|
||||||
|
return p.dataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommonOAuth2Provider returns a new common OAuth 2.0 provider
|
||||||
|
func NewCommonOAuth2Provider(config *settings.Config, redirectUrl string, dataSource CommonOAuth2DataSource) *CommonOAuth2Provider {
|
||||||
|
oauth2Config := &oauth2.Config{
|
||||||
|
ClientID: config.OAuth2ClientID,
|
||||||
|
ClientSecret: config.OAuth2ClientSecret,
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: dataSource.GetAuthUrl(),
|
||||||
|
TokenURL: dataSource.GetTokenUrl(),
|
||||||
|
},
|
||||||
|
RedirectURL: redirectUrl,
|
||||||
|
Scopes: dataSource.GetScopes(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CommonOAuth2Provider{
|
||||||
|
oauth2Config: oauth2Config,
|
||||||
|
dataSource: dataSource,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package gitea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/common"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type giteaUserInfoResponse struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GiteaOAuth2DataSource represents Gitea OAuth 2.0 data source
|
||||||
|
type GiteaOAuth2DataSource struct {
|
||||||
|
common.CommonOAuth2DataSource
|
||||||
|
baseUrl string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthUrl returns the authentication url of the Gitea data source
|
||||||
|
func (s *GiteaOAuth2DataSource) GetAuthUrl() string {
|
||||||
|
// Reference: https://docs.gitea.com/development/oauth2-provider
|
||||||
|
return s.baseUrl + "login/oauth/authorize"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenUrl returns the token url of the Gitea data source
|
||||||
|
func (s *GiteaOAuth2DataSource) GetTokenUrl() string {
|
||||||
|
// Reference: https://docs.gitea.com/development/oauth2-provider
|
||||||
|
return s.baseUrl + "login/oauth/access_token"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfoRequest returns the user info request of the Gitea data source
|
||||||
|
func (s *GiteaOAuth2DataSource) GetUserInfoRequest() (*http.Request, error) {
|
||||||
|
// Reference: https://gitea.com/api/swagger#/user/userGetCurrent
|
||||||
|
req, err := http.NewRequest("GET", s.baseUrl+"api/v1/user", nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScopes returns the scopes required by the Gitea provider
|
||||||
|
func (s *GiteaOAuth2DataSource) GetScopes() []string {
|
||||||
|
return []string{"read:user"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseUserInfo returns the user info by parsing the response body
|
||||||
|
func (s *GiteaOAuth2DataSource) ParseUserInfo(c core.Context, body []byte) (*data.OAuth2UserInfo, error) {
|
||||||
|
userInfoResp := &giteaUserInfoResponse{}
|
||||||
|
err := json.Unmarshal(body, &userInfoResp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[gitea_oauth2_datasource.ParseUserInfo] failed to parse user profile response body, because %s", err.Error())
|
||||||
|
return nil, errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if userInfoResp.Login == "" {
|
||||||
|
log.Warnf(c, "[gitea_oauth2_datasource.ParseUserInfo] invalid user profile response body")
|
||||||
|
return nil, errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
return &data.OAuth2UserInfo{
|
||||||
|
UserName: userInfoResp.Login,
|
||||||
|
Email: userInfoResp.Email,
|
||||||
|
NickName: userInfoResp.FullName,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGiteaOAuth2Provider creates a new Gitea OAuth 2.0 provider instance
|
||||||
|
func NewGiteaOAuth2Provider(config *settings.Config, redirectUrl string) (provider.OAuth2Provider, error) {
|
||||||
|
if len(config.OAuth2GiteaBaseUrl) < 1 {
|
||||||
|
return nil, errs.ErrInvalidOAuth2Config
|
||||||
|
}
|
||||||
|
|
||||||
|
baseUrl := config.OAuth2GiteaBaseUrl
|
||||||
|
|
||||||
|
if baseUrl[len(baseUrl)-1] != '/' {
|
||||||
|
baseUrl += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return common.NewCommonOAuth2Provider(config, redirectUrl, &GiteaOAuth2DataSource{
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package gitea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/common"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewGiteaOAuth2Provider(t *testing.T) {
|
||||||
|
provider, err := NewGiteaOAuth2Provider(&settings.Config{
|
||||||
|
OAuth2GiteaBaseUrl: "https://example.com/",
|
||||||
|
}, "")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "https://example.com/login/oauth/authorize", provider.(*common.CommonOAuth2Provider).GetDataSource().GetAuthUrl())
|
||||||
|
assert.Equal(t, "https://example.com/login/oauth/access_token", provider.(*common.CommonOAuth2Provider).GetDataSource().GetTokenUrl())
|
||||||
|
|
||||||
|
provider, err = NewGiteaOAuth2Provider(&settings.Config{
|
||||||
|
OAuth2GiteaBaseUrl: "https://example.com",
|
||||||
|
}, "")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "https://example.com/login/oauth/authorize", provider.(*common.CommonOAuth2Provider).GetDataSource().GetAuthUrl())
|
||||||
|
assert.Equal(t, "https://example.com/login/oauth/access_token", provider.(*common.CommonOAuth2Provider).GetDataSource().GetTokenUrl())
|
||||||
|
|
||||||
|
provider, err = NewGiteaOAuth2Provider(&settings.Config{}, "")
|
||||||
|
assert.Equal(t, errs.ErrInvalidOAuth2Config, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGiteaOAuth2Datasource_GetUserInfoRequest(t *testing.T) {
|
||||||
|
datasource := &GiteaOAuth2DataSource{baseUrl: "https://example.com/"}
|
||||||
|
req, err := datasource.GetUserInfoRequest()
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "GET", req.Method)
|
||||||
|
assert.Equal(t, "https://example.com/api/v1/user", req.URL.String())
|
||||||
|
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGiteaOAuth2Datasource_ParseUserInfo_Success(t *testing.T) {
|
||||||
|
datasource := &GiteaOAuth2DataSource{}
|
||||||
|
responseContent := `{
|
||||||
|
"login": "user1",
|
||||||
|
"full_name": "User",
|
||||||
|
"email": "user1@example.com"
|
||||||
|
}`
|
||||||
|
info, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "user1", info.UserName)
|
||||||
|
assert.Equal(t, "user1@example.com", info.Email)
|
||||||
|
assert.Equal(t, "User", info.NickName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGiteaOAuth2Datasource_ParseUserInfo_InvalidJson(t *testing.T) {
|
||||||
|
datasource := &GiteaOAuth2DataSource{}
|
||||||
|
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte("invalid"))
|
||||||
|
|
||||||
|
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGiteaOAuth2Datasource_ParseUserInfo_EmptyLogin(t *testing.T) {
|
||||||
|
datasource := &GiteaOAuth2DataSource{}
|
||||||
|
responseContent := `{"login": ""}`
|
||||||
|
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
|
||||||
|
|
||||||
|
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package github
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const githubOAuth2AuthUrl = "https://github.com/login/oauth/authorize" // Reference: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
|
||||||
|
const githubOAuth2TokenUrl = "https://github.com/login/oauth/access_token" // Reference: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
|
||||||
|
const githubUserProfileApiUrl = "https://api.github.com/user" // Reference: https://docs.github.com/en/rest/users/users
|
||||||
|
const githubUserEmailApiUrl = "https://api.github.com/user/emails" // Reference: https://docs.github.com/en/rest/users/emails
|
||||||
|
|
||||||
|
var githubOAuth2Scopes = []string{"user:email"}
|
||||||
|
|
||||||
|
type githubUserProfileResponse struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type githubUserEmailsResponse struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Primary bool `json:"primary"`
|
||||||
|
Verified bool `json:"verified"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GithubOAuth2Provider represents Github OAuth 2.0 provider
|
||||||
|
type GithubOAuth2Provider struct {
|
||||||
|
provider.OAuth2Provider
|
||||||
|
oauth2Config *oauth2.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOAuth2AuthUrl returns the authentication url of the GitHub OAuth 2.0 provider
|
||||||
|
func (p *GithubOAuth2Provider) GetOAuth2AuthUrl(c core.Context, state string, opts ...oauth2.AuthCodeOption) (string, error) {
|
||||||
|
return p.oauth2Config.AuthCodeURL(state, opts...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOAuth2Token returns the OAuth 2.0 token of the GitHub OAuth 2.0 provider
|
||||||
|
func (p *GithubOAuth2Provider) GetOAuth2Token(c core.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||||
|
return p.oauth2Config.Exchange(c, code, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo returns the user info by the Github OAuth 2.0 provider
|
||||||
|
func (p *GithubOAuth2Provider) GetUserInfo(c core.Context, oauth2Token *oauth2.Token) (*data.OAuth2UserInfo, error) {
|
||||||
|
// first get user name and nick name from user profile
|
||||||
|
req, err := p.buildAPIRequest(githubUserProfileApiUrl)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user info request, because %s", err.Error())
|
||||||
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2Client := oauth2.NewClient(c, oauth2.StaticTokenSource(oauth2Token))
|
||||||
|
resp, err := oauth2Client.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user info response, because %s", err.Error())
|
||||||
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
log.Debugf(c, "[github_oauth2_provider.GetUserInfo] user profile response is %s", body)
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user info response, because response code is %d", resp.StatusCode)
|
||||||
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
|
}
|
||||||
|
|
||||||
|
userProfileResp, err := p.parseUserProfile(c, body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// then get user primary email
|
||||||
|
req, err = p.buildAPIRequest(githubUserEmailApiUrl)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user emails request, because %s", err.Error())
|
||||||
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = oauth2Client.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user emails response, because %s", err.Error())
|
||||||
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err = io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
log.Debugf(c, "[github_oauth2_provider.GetUserInfo] user emails response is %s", body)
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
log.Errorf(c, "[github_oauth2_provider.GetUserInfo] failed to get user emails response, because response code is %d", resp.StatusCode)
|
||||||
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
|
}
|
||||||
|
|
||||||
|
email, err := p.parsePrimaryEmail(c, body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &data.OAuth2UserInfo{
|
||||||
|
UserName: userProfileResp.Login,
|
||||||
|
Email: email,
|
||||||
|
NickName: userProfileResp.Name,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GithubOAuth2Provider) parseUserProfile(c core.Context, body []byte) (*githubUserProfileResponse, error) {
|
||||||
|
userProfileResp := &githubUserProfileResponse{}
|
||||||
|
err := json.Unmarshal(body, &userProfileResp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[github_oauth2_provider.parseUserProfile] failed to parse user profile response body, because %s", err.Error())
|
||||||
|
return nil, errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if userProfileResp.Login == "" {
|
||||||
|
log.Warnf(c, "[github_oauth2_provider.parseUserProfile] invalid user profile response body")
|
||||||
|
return nil, errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
return userProfileResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GithubOAuth2Provider) parsePrimaryEmail(c core.Context, body []byte) (string, error) {
|
||||||
|
emailsResp := make([]githubUserEmailsResponse, 0)
|
||||||
|
err := json.Unmarshal(body, &emailsResp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[github_oauth2_provider.parsePrimaryEmail] failed to parse user emails response body, because %s", err.Error())
|
||||||
|
return "", errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, emailEntry := range emailsResp {
|
||||||
|
if emailEntry.Primary && emailEntry.Verified {
|
||||||
|
return emailEntry.Email, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GithubOAuth2Provider) buildAPIRequest(url string) (*http.Request, error) {
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Accept", "application/vnd.github+json")
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGithubOAuth2Provider creates a new Github OAuth 2.0 provider instance
|
||||||
|
func NewGithubOAuth2Provider(config *settings.Config, redirectUrl string) (provider.OAuth2Provider, error) {
|
||||||
|
oauth2Config := &oauth2.Config{
|
||||||
|
ClientID: config.OAuth2ClientID,
|
||||||
|
ClientSecret: config.OAuth2ClientSecret,
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: githubOAuth2AuthUrl,
|
||||||
|
TokenURL: githubOAuth2TokenUrl,
|
||||||
|
},
|
||||||
|
RedirectURL: redirectUrl,
|
||||||
|
Scopes: githubOAuth2Scopes,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GithubOAuth2Provider{
|
||||||
|
oauth2Config: oauth2Config,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package github
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGithubOAuth2Datasource_ParseUserProfile_Success(t *testing.T) {
|
||||||
|
datasource := &GithubOAuth2Provider{}
|
||||||
|
responseContent := `{
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false,
|
||||||
|
"name": "monalisa octocat",
|
||||||
|
"company": "GitHub",
|
||||||
|
"blog": "https://github.com/blog",
|
||||||
|
"location": "San Francisco",
|
||||||
|
"email": "octocat@github.com",
|
||||||
|
"hireable": false,
|
||||||
|
"bio": "There once was...",
|
||||||
|
"twitter_username": "monatheoctocat",
|
||||||
|
"public_repos": 2,
|
||||||
|
"public_gists": 1,
|
||||||
|
"followers": 20,
|
||||||
|
"following": 0,
|
||||||
|
"created_at": "2008-01-14T04:33:35Z",
|
||||||
|
"updated_at": "2008-01-14T04:33:35Z",
|
||||||
|
"private_gists": 81,
|
||||||
|
"total_private_repos": 100,
|
||||||
|
"owned_private_repos": 100,
|
||||||
|
"disk_usage": 10000,
|
||||||
|
"collaborators": 8,
|
||||||
|
"two_factor_authentication": true,
|
||||||
|
"plan": {
|
||||||
|
"name": "Medium",
|
||||||
|
"space": 400,
|
||||||
|
"private_repos": 20,
|
||||||
|
"collaborators": 0
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
info, err := datasource.parseUserProfile(core.NewNullContext(), []byte(responseContent))
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "octocat", info.Login)
|
||||||
|
assert.Equal(t, "monalisa octocat", info.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGithubOAuth2Datasource_ParseUserProfile_EmptyLogin(t *testing.T) {
|
||||||
|
datasource := &GithubOAuth2Provider{}
|
||||||
|
responseContent := `{"login": ""}`
|
||||||
|
_, err := datasource.parseUserProfile(core.NewNullContext(), []byte(responseContent))
|
||||||
|
|
||||||
|
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGithubOAuth2Datasource_ParsePrimaryEmail(t *testing.T) {
|
||||||
|
datasource := &GithubOAuth2Provider{}
|
||||||
|
responseContent := `[
|
||||||
|
{
|
||||||
|
"email": "foo@bar.com",
|
||||||
|
"primary": false,
|
||||||
|
"verified": true,
|
||||||
|
"visibility": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "octocat@github.com",
|
||||||
|
"primary": true,
|
||||||
|
"verified": true,
|
||||||
|
"visibility": "public"
|
||||||
|
}
|
||||||
|
]`
|
||||||
|
email, err := datasource.parsePrimaryEmail(core.NewNullContext(), []byte(responseContent))
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "octocat@github.com", email)
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package nextcloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/common"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nextcloudUserInfoResponse struct {
|
||||||
|
OCS *struct {
|
||||||
|
Meta *struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
StatusCode int `json:"statuscode"`
|
||||||
|
} `json:"meta"`
|
||||||
|
Data *struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
DisplayName string `json:"display-name"`
|
||||||
|
} `json:"data"`
|
||||||
|
} `json:"ocs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextcloudOAuth2DataSource represents Nextcloud OAuth 2.0 data source
|
||||||
|
type NextcloudOAuth2DataSource struct {
|
||||||
|
common.CommonOAuth2DataSource
|
||||||
|
baseUrl string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthUrl returns the authentication url of the Nextcloud data source
|
||||||
|
func (s *NextcloudOAuth2DataSource) GetAuthUrl() string {
|
||||||
|
// Reference: https://docs.nextcloud.com/server/stable/developer_manual/_static/openapi.html#/operations/oauth2-login_redirector-authorize
|
||||||
|
return s.baseUrl + "apps/oauth2/authorize"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenUrl returns the token url of the Nextcloud data source
|
||||||
|
func (s *NextcloudOAuth2DataSource) GetTokenUrl() string {
|
||||||
|
// Reference: https://docs.nextcloud.com/server/stable/developer_manual/_static/openapi.html#/operations/oauth2-oauth_api-get-token
|
||||||
|
return s.baseUrl + "apps/oauth2/api/v1/token"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfoRequest returns the user info request of the Nextcloud data source
|
||||||
|
func (s *NextcloudOAuth2DataSource) GetUserInfoRequest() (*http.Request, error) {
|
||||||
|
// Reference: https://docs.nextcloud.com/server/stable/developer_manual/_static/openapi.html#/operations/provisioning_api-users-get-current-user
|
||||||
|
req, err := http.NewRequest("GET", s.baseUrl+"ocs/v2.php/cloud/user", nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("OCS-APIRequest", "true")
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScopes returns the scopes required by the Nextcloud provider
|
||||||
|
func (s *NextcloudOAuth2DataSource) GetScopes() []string {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseUserInfo returns the user info by parsing the response body
|
||||||
|
func (s *NextcloudOAuth2DataSource) ParseUserInfo(c core.Context, body []byte) (*data.OAuth2UserInfo, error) {
|
||||||
|
userInfoResp := &nextcloudUserInfoResponse{}
|
||||||
|
err := json.Unmarshal(body, &userInfoResp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[nextcloud_oauth2_datasource.ParseUserInfo] failed to parse user info response body, because %s", err.Error())
|
||||||
|
return nil, errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if userInfoResp.OCS == nil || userInfoResp.OCS.Meta == nil || userInfoResp.OCS.Data == nil {
|
||||||
|
log.Warnf(c, "[nextcloud_oauth2_datasource.ParseUserInfo] invalid user info response body")
|
||||||
|
return nil, errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if userInfoResp.OCS.Meta.StatusCode != 200 {
|
||||||
|
log.Warnf(c, "[nextcloud_oauth2_datasource.ParseUserInfo] user info response status code is %d", userInfoResp.OCS.Meta.StatusCode)
|
||||||
|
return nil, errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if userInfoResp.OCS.Data.ID == "" {
|
||||||
|
log.Warnf(c, "[nextcloud_oauth2_datasource.ParseUserInfo] user info id is empty")
|
||||||
|
return nil, errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
return &data.OAuth2UserInfo{
|
||||||
|
UserName: userInfoResp.OCS.Data.ID,
|
||||||
|
Email: userInfoResp.OCS.Data.Email,
|
||||||
|
NickName: userInfoResp.OCS.Data.DisplayName,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNextcloudOAuth2Provider creates a new Nextcloud OAuth 2.0 provider instance
|
||||||
|
func NewNextcloudOAuth2Provider(config *settings.Config, redirectUrl string) (provider.OAuth2Provider, error) {
|
||||||
|
if len(config.OAuth2NextcloudBaseUrl) < 1 {
|
||||||
|
return nil, errs.ErrInvalidOAuth2Config
|
||||||
|
}
|
||||||
|
|
||||||
|
baseUrl := config.OAuth2NextcloudBaseUrl
|
||||||
|
|
||||||
|
if baseUrl[len(baseUrl)-1] != '/' {
|
||||||
|
baseUrl += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return common.NewCommonOAuth2Provider(config, redirectUrl, &NextcloudOAuth2DataSource{
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package nextcloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider/common"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewNextcloudOAuth2Provider(t *testing.T) {
|
||||||
|
provider, err := NewNextcloudOAuth2Provider(&settings.Config{
|
||||||
|
OAuth2NextcloudBaseUrl: "https://example.com/",
|
||||||
|
}, "")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "https://example.com/apps/oauth2/authorize", provider.(*common.CommonOAuth2Provider).GetDataSource().GetAuthUrl())
|
||||||
|
assert.Equal(t, "https://example.com/apps/oauth2/api/v1/token", provider.(*common.CommonOAuth2Provider).GetDataSource().GetTokenUrl())
|
||||||
|
|
||||||
|
provider, err = NewNextcloudOAuth2Provider(&settings.Config{
|
||||||
|
OAuth2NextcloudBaseUrl: "https://example.com/index.php",
|
||||||
|
}, "")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "https://example.com/index.php/apps/oauth2/authorize", provider.(*common.CommonOAuth2Provider).GetDataSource().GetAuthUrl())
|
||||||
|
assert.Equal(t, "https://example.com/index.php/apps/oauth2/api/v1/token", provider.(*common.CommonOAuth2Provider).GetDataSource().GetTokenUrl())
|
||||||
|
|
||||||
|
provider, err = NewNextcloudOAuth2Provider(&settings.Config{}, "")
|
||||||
|
assert.Equal(t, errs.ErrInvalidOAuth2Config, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextcloudOAuth2Datasource_GetUserInfoRequest(t *testing.T) {
|
||||||
|
datasource := &NextcloudOAuth2DataSource{baseUrl: "https://example.com/"}
|
||||||
|
req, err := datasource.GetUserInfoRequest()
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "GET", req.Method)
|
||||||
|
assert.Equal(t, "https://example.com/ocs/v2.php/cloud/user", req.URL.String())
|
||||||
|
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
||||||
|
assert.Equal(t, "true", req.Header.Get("OCS-APIRequest"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextcloudOAuth2Datasource_ParseUserInfo_Success(t *testing.T) {
|
||||||
|
datasource := &NextcloudOAuth2DataSource{}
|
||||||
|
responseContent := `{
|
||||||
|
"ocs": {
|
||||||
|
"meta": {
|
||||||
|
"status": "ok",
|
||||||
|
"statuscode": 200
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"id": "user1",
|
||||||
|
"email": "user1@example.com",
|
||||||
|
"display-name": "User"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
info, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "user1", info.UserName)
|
||||||
|
assert.Equal(t, "user1@example.com", info.Email)
|
||||||
|
assert.Equal(t, "User", info.NickName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextcloudOAuth2Datasource_ParseUserInfo_InvalidJson(t *testing.T) {
|
||||||
|
datasource := &NextcloudOAuth2DataSource{}
|
||||||
|
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte("invalid"))
|
||||||
|
|
||||||
|
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextcloudOAuth2Datasource_ParseUserInfo_MissingFields(t *testing.T) {
|
||||||
|
datasource := &NextcloudOAuth2DataSource{}
|
||||||
|
responseContent := `{"ocs": {}}`
|
||||||
|
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
|
||||||
|
|
||||||
|
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextcloudOAuth2Datasource_ParseUserInfo_Non200StatusCode(t *testing.T) {
|
||||||
|
datasource := &NextcloudOAuth2DataSource{}
|
||||||
|
responseContent := `{
|
||||||
|
"ocs": {
|
||||||
|
"meta": {
|
||||||
|
"status": "error",
|
||||||
|
"statuscode": 400
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
|
||||||
|
|
||||||
|
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextcloudOAuth2Datasource_ParseUserInfo_EmptyID(t *testing.T) {
|
||||||
|
datasource := &NextcloudOAuth2DataSource{}
|
||||||
|
responseContent := `{
|
||||||
|
"ocs": {
|
||||||
|
"meta": {
|
||||||
|
"status": "ok",
|
||||||
|
"statuscode": 200
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"id": "",
|
||||||
|
"email": "user1@example.com",
|
||||||
|
"display-name": "User One"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
|
||||||
|
|
||||||
|
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OAuth2Provider defines the structure of OAuth 2.0 provider
|
||||||
|
type OAuth2Provider interface {
|
||||||
|
// GetOAuth2AuthUrl returns the authentication url of the provider
|
||||||
|
GetOAuth2AuthUrl(c core.Context, state string, opts ...oauth2.AuthCodeOption) (string, error)
|
||||||
|
|
||||||
|
// GetOAuth2Token returns the OAuth 2.0 token of the provider
|
||||||
|
GetOAuth2Token(c core.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
|
||||||
|
|
||||||
|
// GetUserInfo returns the user info
|
||||||
|
GetUserInfo(c core.Context, oauth2Token *oauth2.Token) (*data.OAuth2UserInfo, error)
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/data"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2/provider"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OIDCClaims represents OIDC claims
|
||||||
|
type OIDCClaims struct {
|
||||||
|
PreferredUserName string `json:"preferred_username"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OIDCProvider represents OIDC provider
|
||||||
|
type OIDCProvider struct {
|
||||||
|
provider.OAuth2Provider
|
||||||
|
oidcIssuerURL string
|
||||||
|
oidcCheckIssuerURL bool
|
||||||
|
redirectUrl string
|
||||||
|
oauth2ClientID string
|
||||||
|
oauth2ClientSecret string
|
||||||
|
oauth2Config *oauth2.Config
|
||||||
|
oidcProvider *oidc.Provider
|
||||||
|
oidcVerifier *oidc.IDTokenVerifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOAuth2AuthUrl returns the authentication url of the OIDC provider
|
||||||
|
func (p *OIDCProvider) GetOAuth2AuthUrl(c core.Context, state string, opts ...oauth2.AuthCodeOption) (string, error) {
|
||||||
|
oauth2Config, err := p.getOAuth2Config(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return oauth2Config.AuthCodeURL(state, opts...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOAuth2Token returns the OAuth 2.0 token of the OIDC provider
|
||||||
|
func (p *OIDCProvider) GetOAuth2Token(c core.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||||
|
oauth2Config, err := p.getOAuth2Config(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return oauth2Config.Exchange(c, code, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo returns the user info by the OIDC provider
|
||||||
|
func (p *OIDCProvider) GetUserInfo(c core.Context, oauth2Token *oauth2.Token) (*data.OAuth2UserInfo, error) {
|
||||||
|
_, err := p.getOAuth2Config(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
log.Errorf(c, "[oidc_provider.GetUserInfo] missing \"id_token\" field in oauth 2.0 token")
|
||||||
|
return nil, errs.ErrInvalidOAuth2Token
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken, err := p.oidcVerifier.Verify(c, rawIDToken)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oidc_provider.GetUserInfo] failed to verify \"id_token\" field in oauth 2.0 token, because %s", err.Error())
|
||||||
|
return nil, errs.ErrInvalidOAuth2Token
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims OIDCClaims
|
||||||
|
err = idToken.Claims(&claims)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oidc_provider.GetUserInfo] failed to parse claims in oauth 2.0 token, because %s", err.Error())
|
||||||
|
return nil, errs.ErrInvalidOAuth2Token
|
||||||
|
}
|
||||||
|
|
||||||
|
userName := claims.PreferredUserName
|
||||||
|
email := claims.Email
|
||||||
|
nickName := claims.Name
|
||||||
|
|
||||||
|
if userName == "" || email == "" || nickName == "" {
|
||||||
|
userInfo, err := p.oidcProvider.UserInfo(c, oauth2.StaticTokenSource(oauth2Token))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oidc_provider.GetUserInfo] failed to get user info, because %s", err.Error())
|
||||||
|
return nil, errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
err = userInfo.Claims(&claims)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oidc_provider.GetUserInfo] failed to parse user info, because %s", err.Error())
|
||||||
|
return nil, errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if userName == "" {
|
||||||
|
userName = claims.PreferredUserName
|
||||||
|
}
|
||||||
|
|
||||||
|
if email == "" {
|
||||||
|
email = claims.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
if nickName == "" {
|
||||||
|
nickName = claims.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &data.OAuth2UserInfo{
|
||||||
|
UserName: userName,
|
||||||
|
Email: email,
|
||||||
|
NickName: nickName,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *OIDCProvider) getOAuth2Config(c core.Context) (*oauth2.Config, error) {
|
||||||
|
if p.oauth2Config != nil {
|
||||||
|
return p.oauth2Config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctx context.Context = c
|
||||||
|
|
||||||
|
if !p.oidcCheckIssuerURL {
|
||||||
|
ctx = oidc.InsecureIssuerURLContext(c, p.oidcIssuerURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
oidcProvider, err := oidc.NewProvider(ctx, p.oidcIssuerURL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[oidc_provider.getOAuth2Config] failed to create oidc provider, because %s", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oidcVerifier := oidcProvider.Verifier(&oidc.Config{
|
||||||
|
ClientID: p.oauth2ClientID,
|
||||||
|
SkipIssuerCheck: !p.oidcCheckIssuerURL,
|
||||||
|
})
|
||||||
|
|
||||||
|
oauth2Config := &oauth2.Config{
|
||||||
|
ClientID: p.oauth2ClientID,
|
||||||
|
ClientSecret: p.oauth2ClientSecret,
|
||||||
|
Endpoint: oidcProvider.Endpoint(),
|
||||||
|
RedirectURL: p.redirectUrl,
|
||||||
|
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||||
|
}
|
||||||
|
|
||||||
|
p.oauth2Config = oauth2Config
|
||||||
|
p.oidcProvider = oidcProvider
|
||||||
|
p.oidcVerifier = oidcVerifier
|
||||||
|
return oauth2Config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOIDCProvider returns a new OIDC provider
|
||||||
|
func NewOIDCProvider(config *settings.Config, redirectUrl string) (*OIDCProvider, error) {
|
||||||
|
if len(config.OAuth2OIDCProviderIssuerURL) < 1 {
|
||||||
|
return nil, errs.ErrInvalidOAuth2Config
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OIDCProvider{
|
||||||
|
oidcIssuerURL: config.OAuth2OIDCProviderIssuerURL,
|
||||||
|
oidcCheckIssuerURL: config.OAuth2OIDCProviderCheckIssuerURL,
|
||||||
|
redirectUrl: redirectUrl,
|
||||||
|
oauth2ClientID: config.OAuth2ClientID,
|
||||||
|
oauth2ClientSecret: config.OAuth2ClientSecret,
|
||||||
|
oauth2Config: nil,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
+15
-7
@@ -91,7 +91,7 @@ func (l *UserDataCli) AddNewUser(c *core.CliContext, username string, email stri
|
|||||||
FeatureRestriction: l.CurrentConfig().DefaultFeatureRestrictions,
|
FeatureRestriction: l.CurrentConfig().DefaultFeatureRestrictions,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := l.users.CreateUser(c, user)
|
err := l.users.CreateUser(c, user, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.CliErrorf(c, "[user_data.AddNewUser] failed to create user \"%s\", because %s", user.Username, err.Error())
|
log.CliErrorf(c, "[user_data.AddNewUser] failed to create user \"%s\", because %s", user.Username, err.Error())
|
||||||
@@ -405,7 +405,7 @@ func (l *UserDataCli) ListUserTokens(c *core.CliContext, username string) ([]*mo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateNewUserToken returns a new token for the specified user
|
// CreateNewUserToken returns a new token for the specified user
|
||||||
func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, tokenType string) (*models.TokenRecord, string, error) {
|
func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, tokenType string, expiresInSeconds int64) (*models.TokenRecord, string, error) {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
log.CliErrorf(c, "[user_data.CreateNewUserToken] user name is empty")
|
log.CliErrorf(c, "[user_data.CreateNewUserToken] user name is empty")
|
||||||
return nil, "", errs.ErrUsernameIsEmpty
|
return nil, "", errs.ErrUsernameIsEmpty
|
||||||
@@ -421,7 +421,17 @@ func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, to
|
|||||||
var token string
|
var token string
|
||||||
var tokenRecord *models.TokenRecord
|
var tokenRecord *models.TokenRecord
|
||||||
|
|
||||||
if tokenType == "mcp" {
|
if tokenType == "api" {
|
||||||
|
if !l.CurrentConfig().EnableAPIToken {
|
||||||
|
return nil, "", errs.ErrAPITokenNotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_GENERATE_API_TOKEN) {
|
||||||
|
return nil, "", errs.ErrNotPermittedToPerformThisAction
|
||||||
|
}
|
||||||
|
|
||||||
|
token, tokenRecord, err = l.tokens.CreateAPITokenViaCli(c, user, expiresInSeconds)
|
||||||
|
} else if tokenType == "mcp" {
|
||||||
if !l.CurrentConfig().EnableMCPServer {
|
if !l.CurrentConfig().EnableMCPServer {
|
||||||
return nil, "", errs.ErrMCPServerNotEnabled
|
return nil, "", errs.ErrMCPServerNotEnabled
|
||||||
}
|
}
|
||||||
@@ -430,9 +440,7 @@ func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, to
|
|||||||
return nil, "", errs.ErrNotPermittedToPerformThisAction
|
return nil, "", errs.ErrNotPermittedToPerformThisAction
|
||||||
}
|
}
|
||||||
|
|
||||||
token, tokenRecord, err = l.tokens.CreateMCPTokenViaCli(c, user)
|
token, tokenRecord, err = l.tokens.CreateMCPTokenViaCli(c, user, expiresInSeconds)
|
||||||
} else if tokenType == "normal" {
|
|
||||||
token, tokenRecord, err = l.tokens.CreateTokenViaCli(c, user)
|
|
||||||
} else {
|
} else {
|
||||||
return nil, "", errs.ErrParameterInvalid
|
return nil, "", errs.ErrParameterInvalid
|
||||||
}
|
}
|
||||||
@@ -447,7 +455,7 @@ func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, to
|
|||||||
|
|
||||||
// RevokeUserToken revokes the specified token of the user
|
// RevokeUserToken revokes the specified token of the user
|
||||||
func (l *UserDataCli) RevokeUserToken(c *core.CliContext, token string) error {
|
func (l *UserDataCli) RevokeUserToken(c *core.CliContext, token string) error {
|
||||||
_, claims, err := l.tokens.ParseToken(c, token)
|
_, claims, _, err := l.tokens.ParseToken(c, token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.CliErrorf(c, "[user_data.RevokeUserToken] failed to parse token, because %s", err.Error())
|
log.CliErrorf(c, "[user_data.RevokeUserToken] failed to parse token, because %s", err.Error())
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ var (
|
|||||||
AlipayAppTransactionDataCsvFileImporter = &alipayAppTransactionDataCsvFileImporter{
|
AlipayAppTransactionDataCsvFileImporter = &alipayAppTransactionDataCsvFileImporter{
|
||||||
alipayTransactionDataCsvFileImporter{
|
alipayTransactionDataCsvFileImporter{
|
||||||
fileHeaderLine: "------------------------------------------------------------------------------------",
|
fileHeaderLine: "------------------------------------------------------------------------------------",
|
||||||
dataHeaderStartContent: "支付宝(中国)网络技术有限公司 电子客户回单",
|
dataHeaderStartContent: []string{"支付宝(中国)网络技术有限公司 电子客户回单", "支付宝支付科技有限公司 电子客户回单"},
|
||||||
originalColumnNames: alipayTransactionColumnNames{
|
originalColumnNames: alipayTransactionColumnNames{
|
||||||
timeColumnName: "交易时间",
|
timeColumnName: "交易时间",
|
||||||
categoryColumnName: "交易分类",
|
categoryColumnName: "交易分类",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ type alipayTransactionColumnNames struct {
|
|||||||
// alipayTransactionDataCsvFileImporter defines the structure of alipay csv importer for transaction data
|
// alipayTransactionDataCsvFileImporter defines the structure of alipay csv importer for transaction data
|
||||||
type alipayTransactionDataCsvFileImporter struct {
|
type alipayTransactionDataCsvFileImporter struct {
|
||||||
fileHeaderLine string
|
fileHeaderLine string
|
||||||
dataHeaderStartContent string
|
dataHeaderStartContent []string
|
||||||
dataBottomEndLineRune rune
|
dataBottomEndLineRune rune
|
||||||
originalColumnNames alipayTransactionColumnNames
|
originalColumnNames alipayTransactionColumnNames
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createNewAlipayTransactionBasicDataTable(ctx core.Context, originalDataTable datatable.BasicDataTable, fileHeaderLine string, dataHeaderStartContent string, dataBottomEndLineRune rune) (datatable.BasicDataTable, error) {
|
func createNewAlipayTransactionBasicDataTable(ctx core.Context, originalDataTable datatable.BasicDataTable, fileHeaderLine string, dataHeaderStartContent []string, dataBottomEndLineRune rune) (datatable.BasicDataTable, error) {
|
||||||
iterator := originalDataTable.DataRowIterator()
|
iterator := originalDataTable.DataRowIterator()
|
||||||
allOriginalLines := make([][]string, 0)
|
allOriginalLines := make([][]string, 0)
|
||||||
hasFileHeader := false
|
hasFileHeader := false
|
||||||
@@ -35,7 +35,7 @@ func createNewAlipayTransactionBasicDataTable(ctx core.Context, originalDataTabl
|
|||||||
if !foundContentBeforeDataHeaderLine {
|
if !foundContentBeforeDataHeaderLine {
|
||||||
if row.ColumnCount() <= 0 {
|
if row.ColumnCount() <= 0 {
|
||||||
continue
|
continue
|
||||||
} else if strings.Index(row.GetData(0), dataHeaderStartContent) >= 0 {
|
} else if utils.ContainsAnyString(row.GetData(0), dataHeaderStartContent) {
|
||||||
foundContentBeforeDataHeaderLine = true
|
foundContentBeforeDataHeaderLine = true
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ var (
|
|||||||
AlipayWebTransactionDataCsvFileImporter = &alipayWebTransactionDataCsvFileImporter{
|
AlipayWebTransactionDataCsvFileImporter = &alipayWebTransactionDataCsvFileImporter{
|
||||||
alipayTransactionDataCsvFileImporter{
|
alipayTransactionDataCsvFileImporter{
|
||||||
fileHeaderLine: "支付宝交易记录明细查询",
|
fileHeaderLine: "支付宝交易记录明细查询",
|
||||||
dataHeaderStartContent: "交易记录明细列表",
|
dataHeaderStartContent: []string{"交易记录明细列表"},
|
||||||
dataBottomEndLineRune: '-',
|
dataBottomEndLineRune: '-',
|
||||||
originalColumnNames: alipayTransactionColumnNames{
|
originalColumnNames: alipayTransactionColumnNames{
|
||||||
timeColumnName: "交易创建时间",
|
timeColumnName: "交易创建时间",
|
||||||
|
|||||||
@@ -261,6 +261,23 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseAccountName(t *testing.T
|
|||||||
assert.Equal(t, 1, len(allNewTransactions))
|
assert.Equal(t, 1, len(allNewTransactions))
|
||||||
assert.Equal(t, "Wallet", allNewTransactions[0].OriginalSourceAccountName)
|
assert.Equal(t, "Wallet", allNewTransactions[0].OriginalSourceAccountName)
|
||||||
assert.Equal(t, "test", allNewTransactions[0].OriginalDestinationAccountName)
|
assert.Equal(t, "test", allNewTransactions[0].OriginalDestinationAccountName)
|
||||||
|
|
||||||
|
// transfer from wechat wallet
|
||||||
|
data5 := "微信支付账单明细,,,,\n" +
|
||||||
|
"微信昵称:[xxx],,,,\n" +
|
||||||
|
"起始时间:[2024-01-01 00:00:00] 终止时间:[2024-09-01 23:59:59],,,,\n" +
|
||||||
|
",,,,\n" +
|
||||||
|
"----------------------微信支付账单明细列表--------------------,,,,\n" +
|
||||||
|
"交易时间,交易类型,收/支,金额(元),支付方式,当前状态\n" +
|
||||||
|
"2024-09-03 23:59:59,信用卡还款,/,¥0.01,零钱,支付成功\n"
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(data5), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 1, len(allNewTransactions))
|
||||||
|
assert.Equal(t, "零钱", allNewTransactions[0].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "", allNewTransactions[0].OriginalDestinationAccountName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWeChatPayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
|
func TestWeChatPayCsvFileImporterParseImportedData_ParseDescription(t *testing.T) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const wechatPayTransactionDescriptionColumnName = "备注"
|
|||||||
|
|
||||||
const wechatPayTransactionDataCategoryTransferToWeChatWallet = "零钱充值"
|
const wechatPayTransactionDataCategoryTransferToWeChatWallet = "零钱充值"
|
||||||
const wechatPayTransactionDataCategoryTransferFromWeChatWallet = "零钱提现"
|
const wechatPayTransactionDataCategoryTransferFromWeChatWallet = "零钱提现"
|
||||||
|
const wechatPayTransactionDataCategoryCreditCardRepayment = "信用卡还款"
|
||||||
|
|
||||||
const wechatPayTransactionDataStatusRefundName = "退款"
|
const wechatPayTransactionDataStatusRefundName = "退款"
|
||||||
|
|
||||||
@@ -125,6 +126,9 @@ func (p *weChatPayTransactionDataRowParser) Parse(ctx core.Context, user *models
|
|||||||
} else if data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] == wechatPayTransactionDataCategoryTransferFromWeChatWallet {
|
} else if data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] == wechatPayTransactionDataCategoryTransferFromWeChatWallet {
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.WeChatWallet
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = localeTextItems.DataConverterTextItems.WeChatWallet
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = relatedAccountName
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = relatedAccountName
|
||||||
|
} else if data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] == wechatPayTransactionDataCategoryCreditCardRepayment {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = relatedAccountName
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
|
||||||
} else {
|
} else {
|
||||||
log.Warnf(ctx, "[wechat_pay_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because unknown transfer transaction category \"%s\"", rowId, data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY])
|
log.Warnf(ctx, "[wechat_pay_transaction_data_row_parser.Parse] skip parsing transaction in row \"%s\", because unknown transfer transaction category \"%s\"", rowId, data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY])
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ func (c *CliContext) Int(name string) int {
|
|||||||
return c.command.Int(name)
|
return c.command.Int(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Int64 returns the long integer value of parameter
|
||||||
|
func (c *CliContext) Int64(name string) int64 {
|
||||||
|
return c.command.Int64(name)
|
||||||
|
}
|
||||||
|
|
||||||
// String returns the string value of parameter
|
// String returns the string value of parameter
|
||||||
func (c *CliContext) String(name string) string {
|
func (c *CliContext) String(name string) string {
|
||||||
return c.command.String(name)
|
return c.command.String(name)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
const webContextRequestIdFieldKey = "REQUEST_ID"
|
const webContextRequestIdFieldKey = "REQUEST_ID"
|
||||||
const webContextTextualTokenFieldKey = "TOKEN_STRING"
|
const webContextTextualTokenFieldKey = "TOKEN_STRING"
|
||||||
const webContextTokenClaimsFieldKey = "TOKEN_CLAIMS"
|
const webContextTokenClaimsFieldKey = "TOKEN_CLAIMS"
|
||||||
|
const webContextTokenContextFieldKey = "TOKEN_CONTEXT"
|
||||||
const webContextResponseErrorFieldKey = "RESPONSE_ERROR"
|
const webContextResponseErrorFieldKey = "RESPONSE_ERROR"
|
||||||
|
|
||||||
// AcceptLanguageHeaderName represents the header name of accept language
|
// AcceptLanguageHeaderName represents the header name of accept language
|
||||||
@@ -113,6 +114,22 @@ func (c *WebContext) GetTokenClaims() *UserTokenClaims {
|
|||||||
return claims.(*UserTokenClaims)
|
return claims.(*UserTokenClaims)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTokenContext sets the given user token context to context
|
||||||
|
func (c *WebContext) SetTokenContext(context string) {
|
||||||
|
c.Set(webContextTokenContextFieldKey, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenContext returns the current user token context
|
||||||
|
func (c *WebContext) GetTokenContext() string {
|
||||||
|
context, exists := c.Get(webContextTokenContextFieldKey)
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.(string)
|
||||||
|
}
|
||||||
|
|
||||||
// GetCurrentUid returns the current user uid by the current user token
|
// GetCurrentUid returns the current user uid by the current user token
|
||||||
func (c *WebContext) GetCurrentUid() int64 {
|
func (c *WebContext) GetCurrentUid() int64 {
|
||||||
claims := c.GetTokenClaims()
|
claims := c.GetTokenClaims()
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ type CliHandlerFunc func(*CliContext) error
|
|||||||
// MiddlewareHandlerFunc represents the middleware handler function
|
// MiddlewareHandlerFunc represents the middleware handler function
|
||||||
type MiddlewareHandlerFunc func(*WebContext)
|
type MiddlewareHandlerFunc func(*WebContext)
|
||||||
|
|
||||||
|
// RedirectHandlerFunc represents the redirect handler function
|
||||||
|
type RedirectHandlerFunc func(*WebContext) (string, *errs.Error)
|
||||||
|
|
||||||
// ApiHandlerFunc represents the api handler function
|
// ApiHandlerFunc represents the api handler function
|
||||||
type ApiHandlerFunc func(*WebContext) (any, *errs.Error)
|
type ApiHandlerFunc func(*WebContext) (any, *errs.Error)
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ type TokenType byte
|
|||||||
|
|
||||||
// Token types
|
// Token types
|
||||||
const (
|
const (
|
||||||
USER_TOKEN_TYPE_NORMAL TokenType = 1
|
USER_TOKEN_TYPE_NORMAL TokenType = 1
|
||||||
USER_TOKEN_TYPE_REQUIRE_2FA TokenType = 2
|
USER_TOKEN_TYPE_REQUIRE_2FA TokenType = 2
|
||||||
USER_TOKEN_TYPE_EMAIL_VERIFY TokenType = 3
|
USER_TOKEN_TYPE_EMAIL_VERIFY TokenType = 3
|
||||||
USER_TOKEN_TYPE_PASSWORD_RESET TokenType = 4
|
USER_TOKEN_TYPE_PASSWORD_RESET TokenType = 4
|
||||||
USER_TOKEN_TYPE_MCP TokenType = 5
|
USER_TOKEN_TYPE_MCP TokenType = 5
|
||||||
|
USER_TOKEN_TYPE_OAUTH2_CALLBACK_REQUIRE_VERIFY TokenType = 6
|
||||||
|
USER_TOKEN_TYPE_OAUTH2_CALLBACK TokenType = 7
|
||||||
|
USER_TOKEN_TYPE_API TokenType = 8
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserTokenClaims represents user token
|
// UserTokenClaims represents user token
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
const USER_EXTERNAL_AUTH_TYPE_CATEOGRY_OAUTH2 = "oauth2"
|
||||||
|
|
||||||
|
// UserExternalAuthType represents the type of user external authentication
|
||||||
|
type UserExternalAuthType string
|
||||||
|
|
||||||
|
// User External Auth Type
|
||||||
|
const (
|
||||||
|
USER_EXTERNAL_AUTH_TYPE_OAUTH2_OIDC UserExternalAuthType = "oidc"
|
||||||
|
USER_EXTERNAL_AUTH_TYPE_OAUTH2_NEXTCLOUD UserExternalAuthType = "nextcloud"
|
||||||
|
USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITEA UserExternalAuthType = "gitea"
|
||||||
|
USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITHUB UserExternalAuthType = "github"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetCategory returns the category of the UserExternalAuthType
|
||||||
|
func (t UserExternalAuthType) GetCategory() string {
|
||||||
|
switch t {
|
||||||
|
case USER_EXTERNAL_AUTH_TYPE_OAUTH2_OIDC,
|
||||||
|
USER_EXTERNAL_AUTH_TYPE_OAUTH2_NEXTCLOUD,
|
||||||
|
USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITEA,
|
||||||
|
USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITHUB:
|
||||||
|
return USER_EXTERNAL_AUTH_TYPE_CATEOGRY_OAUTH2
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid checks if the UserExternalAuthType is valid
|
||||||
|
func (t UserExternalAuthType) IsValid() bool {
|
||||||
|
return t.GetCategory() != ""
|
||||||
|
}
|
||||||
@@ -90,10 +90,13 @@ const (
|
|||||||
USER_FEATURE_RESTRICTION_TYPE_SYNC_APPLICATION_SETTINGS UserFeatureRestrictionType = 12
|
USER_FEATURE_RESTRICTION_TYPE_SYNC_APPLICATION_SETTINGS UserFeatureRestrictionType = 12
|
||||||
USER_FEATURE_RESTRICTION_TYPE_MCP_ACCESS UserFeatureRestrictionType = 13
|
USER_FEATURE_RESTRICTION_TYPE_MCP_ACCESS UserFeatureRestrictionType = 13
|
||||||
USER_FEATURE_RESTRICTION_TYPE_CREATE_TRANSACTION_FROM_AI_IMAGE_RECOGNITION UserFeatureRestrictionType = 14
|
USER_FEATURE_RESTRICTION_TYPE_CREATE_TRANSACTION_FROM_AI_IMAGE_RECOGNITION UserFeatureRestrictionType = 14
|
||||||
|
USER_FEATURE_RESTRICTION_TYPE_OAUTH2_LOGIN UserFeatureRestrictionType = 15
|
||||||
|
USER_FEATURE_RESTRICTION_TYPE_UNLINK_THIRD_PARTY_LOGIN UserFeatureRestrictionType = 16
|
||||||
|
USER_FEATURE_RESTRICTION_TYPE_GENERATE_API_TOKEN UserFeatureRestrictionType = 17
|
||||||
)
|
)
|
||||||
|
|
||||||
const userFeatureRestrictionTypeMinValue UserFeatureRestrictionType = USER_FEATURE_RESTRICTION_TYPE_UPDATE_PASSWORD
|
const userFeatureRestrictionTypeMinValue UserFeatureRestrictionType = USER_FEATURE_RESTRICTION_TYPE_UPDATE_PASSWORD
|
||||||
const userFeatureRestrictionTypeMaxValue UserFeatureRestrictionType = USER_FEATURE_RESTRICTION_TYPE_CREATE_TRANSACTION_FROM_AI_IMAGE_RECOGNITION
|
const userFeatureRestrictionTypeMaxValue UserFeatureRestrictionType = USER_FEATURE_RESTRICTION_TYPE_GENERATE_API_TOKEN
|
||||||
|
|
||||||
// String returns a textual representation of the restriction type of user features
|
// String returns a textual representation of the restriction type of user features
|
||||||
func (t UserFeatureRestrictionType) String() string {
|
func (t UserFeatureRestrictionType) String() string {
|
||||||
@@ -124,6 +127,14 @@ func (t UserFeatureRestrictionType) String() string {
|
|||||||
return "Sync Application Settings"
|
return "Sync Application Settings"
|
||||||
case USER_FEATURE_RESTRICTION_TYPE_MCP_ACCESS:
|
case USER_FEATURE_RESTRICTION_TYPE_MCP_ACCESS:
|
||||||
return "MCP (Model Context Protocol) Access"
|
return "MCP (Model Context Protocol) Access"
|
||||||
|
case USER_FEATURE_RESTRICTION_TYPE_CREATE_TRANSACTION_FROM_AI_IMAGE_RECOGNITION:
|
||||||
|
return "Create Transaction from AI Image Recognition"
|
||||||
|
case USER_FEATURE_RESTRICTION_TYPE_OAUTH2_LOGIN:
|
||||||
|
return "OAuth 2.0 Login"
|
||||||
|
case USER_FEATURE_RESTRICTION_TYPE_UNLINK_THIRD_PARTY_LOGIN:
|
||||||
|
return "Unlink Third-Party Login"
|
||||||
|
case USER_FEATURE_RESTRICTION_TYPE_GENERATE_API_TOKEN:
|
||||||
|
return "Generate API Token"
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("Invalid(%d)", int(t))
|
return fmt.Sprintf("Invalid(%d)", int(t))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import "time"
|
|||||||
type DuplicateChecker interface {
|
type DuplicateChecker interface {
|
||||||
GetSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string) (bool, string)
|
GetSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string) (bool, string)
|
||||||
SetSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string, remark string)
|
SetSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string, remark string)
|
||||||
|
SetSubmissionRemarkWithCustomExpiration(checkerType DuplicateCheckerType, uid int64, identification string, remark string, expiration time.Duration)
|
||||||
RemoveSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string)
|
RemoveSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string)
|
||||||
GetOrSetCronJobRunningInfo(jobName string, runningInfo string, runningInterval time.Duration) (bool, string)
|
GetOrSetCronJobRunningInfo(jobName string, runningInfo string, runningInterval time.Duration) (bool, string)
|
||||||
RemoveCronJobRunningInfo(jobName string)
|
RemoveCronJobRunningInfo(jobName string)
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ func (c *DuplicateCheckerContainer) SetSubmissionRemark(checkerType DuplicateChe
|
|||||||
c.current.SetSubmissionRemark(checkerType, uid, identification, remark)
|
c.current.SetSubmissionRemark(checkerType, uid, identification, remark)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSubmissionRemarkWithCustomExpiration saves the identification and remark by the current duplicate checker with custom expiration time
|
||||||
|
func (c *DuplicateCheckerContainer) SetSubmissionRemarkWithCustomExpiration(checkerType DuplicateCheckerType, uid int64, identification string, remark string, expiration time.Duration) {
|
||||||
|
if c.current == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.current.SetSubmissionRemarkWithCustomExpiration(checkerType, uid, identification, remark, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
// RemoveSubmissionRemark removes the identification and remark by the current duplicate checker
|
// RemoveSubmissionRemark removes the identification and remark by the current duplicate checker
|
||||||
func (c *DuplicateCheckerContainer) RemoveSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string) {
|
func (c *DuplicateCheckerContainer) RemoveSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string) {
|
||||||
if c.current == nil {
|
if c.current == nil {
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ const (
|
|||||||
DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE DuplicateCheckerType = 5
|
DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE DuplicateCheckerType = 5
|
||||||
DUPLICATE_CHECKER_TYPE_NEW_PICTURE DuplicateCheckerType = 6
|
DUPLICATE_CHECKER_TYPE_NEW_PICTURE DuplicateCheckerType = 6
|
||||||
DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS DuplicateCheckerType = 7
|
DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS DuplicateCheckerType = 7
|
||||||
|
DUPLICATE_CHECKER_TYPE_OAUTH2_REDIRECT DuplicateCheckerType = 8
|
||||||
DUPLICATE_CHECKER_TYPE_FAILURE_CHECK DuplicateCheckerType = 255
|
DUPLICATE_CHECKER_TYPE_FAILURE_CHECK DuplicateCheckerType = 255
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ func (c *InMemoryDuplicateChecker) SetSubmissionRemark(checkerType DuplicateChec
|
|||||||
c.cache.Set(c.getCacheKey(checkerType, uid, identification), remark, cache.DefaultExpiration)
|
c.cache.Set(c.getCacheKey(checkerType, uid, identification), remark, cache.DefaultExpiration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSubmissionRemarkWithCustomExpiration saves the identification and remark to in-memory cache with custom expiration time
|
||||||
|
func (c *InMemoryDuplicateChecker) SetSubmissionRemarkWithCustomExpiration(checkerType DuplicateCheckerType, uid int64, identification string, remark string, expiration time.Duration) {
|
||||||
|
c.cache.Set(c.getCacheKey(checkerType, uid, identification), remark, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
// RemoveSubmissionRemark removes the identification and remark in in-memory cache
|
// RemoveSubmissionRemark removes the identification and remark in in-memory cache
|
||||||
func (c *InMemoryDuplicateChecker) RemoveSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string) {
|
func (c *InMemoryDuplicateChecker) RemoveSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string) {
|
||||||
c.cache.Delete(c.getCacheKey(checkerType, uid, identification))
|
c.cache.Delete(c.getCacheKey(checkerType, uid, identification))
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ const (
|
|||||||
NormalSubcategoryUserCustomExchangeRate = 13
|
NormalSubcategoryUserCustomExchangeRate = 13
|
||||||
NormalSubcategoryModelContextProtocol = 14
|
NormalSubcategoryModelContextProtocol = 14
|
||||||
NormalSubcategoryLargeLanguageModel = 15
|
NormalSubcategoryLargeLanguageModel = 15
|
||||||
|
NormalSubcategoryUserExternalAuth = 16
|
||||||
|
NormalSubcategoryOAuth2 = 17
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error represents the specific error returned to user
|
// Error represents the specific error returned to user
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package errs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error codes related to user external authentication
|
||||||
|
var (
|
||||||
|
ErrUserExternalAuthNotFound = NewNormalError(NormalSubcategoryUserExternalAuth, 0, http.StatusBadRequest, "user external auth is not found")
|
||||||
|
ErrUserExternalAuthAlreadyExists = NewNormalError(NormalSubcategoryUserExternalAuth, 1, http.StatusBadRequest, "user external auth already exists")
|
||||||
|
ErrUserExternalAuthTypeInvalid = NewNormalError(NormalSubcategoryUserExternalAuth, 2, http.StatusBadRequest, "user external auth type invalid")
|
||||||
|
)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package errs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error codes related to oauth 2.0
|
||||||
|
var (
|
||||||
|
ErrOAuth2NotEnabled = NewNormalError(NormalSubcategoryOAuth2, 0, http.StatusBadRequest, "oauth2 not enabled")
|
||||||
|
ErrOAuth2AutoRegistrationNotEnabled = NewNormalError(NormalSubcategoryOAuth2, 1, http.StatusBadRequest, "oauth2 auto registration not enabled")
|
||||||
|
ErrInvalidOAuth2LoginRequest = NewNormalError(NormalSubcategoryOAuth2, 2, http.StatusBadRequest, "invalid oauth2 login request")
|
||||||
|
ErrInvalidOAuth2Callback = NewNormalError(NormalSubcategoryOAuth2, 3, http.StatusBadRequest, "invalid oauth2 callback")
|
||||||
|
ErrMissingOAuth2State = NewNormalError(NormalSubcategoryOAuth2, 4, http.StatusBadRequest, "missing state in oauth2 callback")
|
||||||
|
ErrMissingOAuth2Code = NewNormalError(NormalSubcategoryOAuth2, 5, http.StatusBadRequest, "missing code in oauth2 callback")
|
||||||
|
ErrInvalidOAuth2State = NewNormalError(NormalSubcategoryOAuth2, 6, http.StatusBadRequest, "invalid state in oauth2 callback")
|
||||||
|
ErrCannotRetrieveOAuth2Token = NewNormalError(NormalSubcategoryOAuth2, 7, http.StatusBadRequest, "cannot retrieve oauth2 token")
|
||||||
|
ErrInvalidOAuth2Token = NewNormalError(NormalSubcategoryOAuth2, 8, http.StatusBadRequest, "invalid oauth2 token")
|
||||||
|
ErrCannotRetrieveUserInfo = NewNormalError(NormalSubcategoryOAuth2, 9, http.StatusBadRequest, "cannot retrieve user info from oauth2 provider")
|
||||||
|
ErrOAuth2UserAlreadyBoundToAnotherUser = NewNormalError(NormalSubcategoryOAuth2, 10, http.StatusBadRequest, "oauth2 user already bound to another user")
|
||||||
|
)
|
||||||
@@ -26,4 +26,8 @@ var (
|
|||||||
ErrInvalidIpAddressPattern = NewSystemError(SystemSubcategorySetting, 19, http.StatusInternalServerError, "invalid ip address pattern")
|
ErrInvalidIpAddressPattern = NewSystemError(SystemSubcategorySetting, 19, http.StatusInternalServerError, "invalid ip address pattern")
|
||||||
ErrInvalidLLMProvider = NewSystemError(SystemSubcategorySetting, 20, http.StatusInternalServerError, "invalid llm provider")
|
ErrInvalidLLMProvider = NewSystemError(SystemSubcategorySetting, 20, http.StatusInternalServerError, "invalid llm provider")
|
||||||
ErrInvalidLLMModelId = NewSystemError(SystemSubcategorySetting, 21, http.StatusInternalServerError, "invalid llm model id")
|
ErrInvalidLLMModelId = NewSystemError(SystemSubcategorySetting, 21, http.StatusInternalServerError, "invalid llm model id")
|
||||||
|
ErrInvalidOAuth2Config = NewSystemError(SystemSubcategorySetting, 22, http.StatusInternalServerError, "invalid oauth 2.0 config")
|
||||||
|
ErrInvalidOAuth2UserIdentifier = NewSystemError(SystemSubcategorySetting, 23, http.StatusInternalServerError, "invalid oauth 2.0 user identifier")
|
||||||
|
ErrInvalidOAuth2Provider = NewSystemError(SystemSubcategorySetting, 24, http.StatusInternalServerError, "invalid oauth 2.0 provider")
|
||||||
|
ErrInvalidOAuth2StateExpiredTime = NewSystemError(SystemSubcategorySetting, 25, http.StatusInternalServerError, "invalid oauth 2.0 state expired time")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,4 +21,5 @@ var (
|
|||||||
ErrTokenIsEmpty = NewNormalError(NormalSubcategoryToken, 12, http.StatusBadRequest, "token is empty")
|
ErrTokenIsEmpty = NewNormalError(NormalSubcategoryToken, 12, http.StatusBadRequest, "token is empty")
|
||||||
ErrEmailVerifyTokenIsInvalidOrExpired = NewNormalError(NormalSubcategoryToken, 13, http.StatusBadRequest, "email verify token is invalid or expired")
|
ErrEmailVerifyTokenIsInvalidOrExpired = NewNormalError(NormalSubcategoryToken, 13, http.StatusBadRequest, "email verify token is invalid or expired")
|
||||||
ErrPasswordResetTokenIsInvalidOrExpired = NewNormalError(NormalSubcategoryToken, 14, http.StatusBadRequest, "password reset token is invalid or expired")
|
ErrPasswordResetTokenIsInvalidOrExpired = NewNormalError(NormalSubcategoryToken, 14, http.StatusBadRequest, "password reset token is invalid or expired")
|
||||||
|
ErrAPITokenNotEnabled = NewNormalError(NormalSubcategoryToken, 15, http.StatusForbidden, "api token is not enabled")
|
||||||
)
|
)
|
||||||
|
|||||||
+41
-37
@@ -4,41 +4,45 @@ import "net/http"
|
|||||||
|
|
||||||
// Error codes related to transaction
|
// Error codes related to transaction
|
||||||
var (
|
var (
|
||||||
ErrTransactionIdInvalid = NewNormalError(NormalSubcategoryTransaction, 0, http.StatusBadRequest, "transaction id is invalid")
|
ErrTransactionIdInvalid = NewNormalError(NormalSubcategoryTransaction, 0, http.StatusBadRequest, "transaction id is invalid")
|
||||||
ErrTransactionNotFound = NewNormalError(NormalSubcategoryTransaction, 1, http.StatusBadRequest, "transaction not found")
|
ErrTransactionNotFound = NewNormalError(NormalSubcategoryTransaction, 1, http.StatusBadRequest, "transaction not found")
|
||||||
ErrTransactionTypeInvalid = NewNormalError(NormalSubcategoryTransaction, 2, http.StatusBadRequest, "transaction type is invalid")
|
ErrTransactionTypeInvalid = NewNormalError(NormalSubcategoryTransaction, 2, http.StatusBadRequest, "transaction type is invalid")
|
||||||
ErrTransactionSourceAndDestinationIdCannotBeEqual = NewNormalError(NormalSubcategoryTransaction, 3, http.StatusBadRequest, "transaction source and destination account id cannot be equal")
|
ErrTransactionSourceAndDestinationIdCannotBeEqual = NewNormalError(NormalSubcategoryTransaction, 3, http.StatusBadRequest, "transaction source and destination account id cannot be equal")
|
||||||
ErrTransactionSourceAndDestinationAmountNotEqual = NewNormalError(NormalSubcategoryTransaction, 4, http.StatusBadRequest, "transaction source and destination amount not equal")
|
ErrTransactionSourceAndDestinationAmountNotEqual = NewNormalError(NormalSubcategoryTransaction, 4, http.StatusBadRequest, "transaction source and destination amount not equal")
|
||||||
ErrTransactionDestinationAccountCannotBeSet = NewNormalError(NormalSubcategoryTransaction, 5, http.StatusBadRequest, "transaction destination account cannot be set")
|
ErrTransactionDestinationAccountCannotBeSet = NewNormalError(NormalSubcategoryTransaction, 5, http.StatusBadRequest, "transaction destination account cannot be set")
|
||||||
ErrTransactionDestinationAmountCannotBeSet = NewNormalError(NormalSubcategoryTransaction, 6, http.StatusBadRequest, "transaction destination amount cannot be set")
|
ErrTransactionDestinationAmountCannotBeSet = NewNormalError(NormalSubcategoryTransaction, 6, http.StatusBadRequest, "transaction destination amount cannot be set")
|
||||||
ErrTooMuchTransactionInOneSecond = NewNormalError(NormalSubcategoryTransaction, 7, http.StatusBadRequest, "too much transaction in one second")
|
ErrTooMuchTransactionInOneSecond = NewNormalError(NormalSubcategoryTransaction, 7, http.StatusBadRequest, "too much transaction in one second")
|
||||||
ErrBalanceModificationTransactionCannotSetCategory = NewNormalError(NormalSubcategoryTransaction, 8, http.StatusBadRequest, "balance modification transaction cannot set category")
|
ErrBalanceModificationTransactionCannotSetCategory = NewNormalError(NormalSubcategoryTransaction, 8, http.StatusBadRequest, "balance modification transaction cannot set category")
|
||||||
ErrBalanceModificationTransactionCannotChangeAccountId = NewNormalError(NormalSubcategoryTransaction, 9, http.StatusBadRequest, "balance modification transaction cannot change account id")
|
ErrBalanceModificationTransactionCannotChangeAccountId = NewNormalError(NormalSubcategoryTransaction, 9, http.StatusBadRequest, "balance modification transaction cannot change account id")
|
||||||
ErrBalanceModificationTransactionCannotAddWhenNotEmpty = NewNormalError(NormalSubcategoryTransaction, 10, http.StatusBadRequest, "balance modification transaction cannot add when other transaction exists")
|
ErrBalanceModificationTransactionCannotAddWhenNotEmpty = NewNormalError(NormalSubcategoryTransaction, 10, http.StatusBadRequest, "balance modification transaction cannot add when other transaction exists")
|
||||||
ErrCannotAddTransactionToHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 11, http.StatusBadRequest, "cannot add transaction to hidden account")
|
ErrCannotAddTransactionToHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 11, http.StatusBadRequest, "cannot add transaction to hidden account")
|
||||||
ErrCannotModifyTransactionInHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 12, http.StatusBadRequest, "cannot modify transaction of 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")
|
ErrCannotDeleteTransactionInHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 13, http.StatusBadRequest, "cannot delete transaction in hidden account")
|
||||||
ErrCannotAddTransactionToParentAccount = NewNormalError(NormalSubcategoryTransaction, 14, http.StatusBadRequest, "cannot add transaction to parent 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")
|
ErrCannotModifyTransactionInParentAccount = NewNormalError(NormalSubcategoryTransaction, 15, http.StatusBadRequest, "cannot modify transaction of parent account")
|
||||||
ErrCannotDeleteTransactionInParentAccount = NewNormalError(NormalSubcategoryTransaction, 16, http.StatusBadRequest, "cannot delete transaction in 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")
|
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")
|
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")
|
ErrCannotDeleteTransactionWithThisTransactionTime = NewNormalError(NormalSubcategoryTransaction, 19, http.StatusBadRequest, "cannot delete transaction with this transaction time")
|
||||||
ErrCannotUseHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 20, http.StatusBadRequest, "cannot use hidden account")
|
ErrCannotUseHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 20, http.StatusBadRequest, "cannot use hidden account")
|
||||||
ErrCannotUseHiddenTransactionCategory = NewNormalError(NormalSubcategoryTransaction, 21, http.StatusBadRequest, "cannot use hidden transaction category")
|
ErrCannotUseHiddenTransactionCategory = NewNormalError(NormalSubcategoryTransaction, 21, http.StatusBadRequest, "cannot use hidden transaction category")
|
||||||
ErrCannotUseHiddenTransactionTag = NewNormalError(NormalSubcategoryTransaction, 22, http.StatusBadRequest, "cannot use hidden transaction tag")
|
ErrCannotUseHiddenTransactionTag = NewNormalError(NormalSubcategoryTransaction, 22, http.StatusBadRequest, "cannot use hidden transaction tag")
|
||||||
ErrTransactionHasTooManyTags = NewNormalError(NormalSubcategoryTransaction, 23, http.StatusBadRequest, "transaction has too many tags")
|
ErrTransactionHasTooManyTags = NewNormalError(NormalSubcategoryTransaction, 23, http.StatusBadRequest, "transaction has too many tags")
|
||||||
ErrTransactionHasTooManyPictures = NewNormalError(NormalSubcategoryTransaction, 24, http.StatusBadRequest, "transaction has too many pictures")
|
ErrTransactionHasTooManyPictures = NewNormalError(NormalSubcategoryTransaction, 24, http.StatusBadRequest, "transaction has too many pictures")
|
||||||
ErrImportFileTypeIsEmpty = NewSystemError(NormalSubcategoryTransaction, 25, http.StatusBadRequest, "import file type is empty")
|
ErrImportFileTypeIsEmpty = NewNormalError(NormalSubcategoryTransaction, 25, http.StatusBadRequest, "import file type is empty")
|
||||||
ErrImportFileTypeNotSupported = NewSystemError(NormalSubcategoryTransaction, 26, http.StatusBadRequest, "import file type not supported")
|
ErrImportFileTypeNotSupported = NewNormalError(NormalSubcategoryTransaction, 26, http.StatusBadRequest, "import file type not supported")
|
||||||
ErrNoDataToImport = NewSystemError(NormalSubcategoryTransaction, 27, http.StatusBadRequest, "no data to import")
|
ErrNoDataToImport = NewNormalError(NormalSubcategoryTransaction, 27, http.StatusBadRequest, "no data to import")
|
||||||
ErrCannotAddTransactionBeforeBalanceModificationTransaction = NewSystemError(NormalSubcategoryTransaction, 28, http.StatusBadRequest, "cannot add transaction before balance modification transaction")
|
ErrCannotAddTransactionBeforeBalanceModificationTransaction = NewNormalError(NormalSubcategoryTransaction, 28, http.StatusBadRequest, "cannot add transaction before balance modification transaction")
|
||||||
ErrBalanceModificationTransactionCannotModifyTime = NewSystemError(NormalSubcategoryTransaction, 29, http.StatusBadRequest, "balance modification transaction cannot modify transaction time")
|
ErrBalanceModificationTransactionCannotModifyTime = NewNormalError(NormalSubcategoryTransaction, 29, http.StatusBadRequest, "balance modification transaction cannot modify transaction time")
|
||||||
ErrTransferTransactionAmountCannotBeLessThanZero = NewNormalError(NormalSubcategoryTransaction, 30, http.StatusBadRequest, "transfer transaction amount cannot be less than zero")
|
ErrTransferTransactionAmountCannotBeLessThanZero = NewNormalError(NormalSubcategoryTransaction, 30, http.StatusBadRequest, "transfer transaction amount cannot be less than zero")
|
||||||
ErrImportFileEncodingIsEmpty = NewSystemError(NormalSubcategoryTransaction, 31, http.StatusBadRequest, "import file encoding is empty")
|
ErrImportFileEncodingIsEmpty = NewNormalError(NormalSubcategoryTransaction, 31, http.StatusBadRequest, "import file encoding is empty")
|
||||||
ErrImportFileEncodingNotSupported = NewSystemError(NormalSubcategoryTransaction, 32, http.StatusBadRequest, "import file encoding not supported")
|
ErrImportFileEncodingNotSupported = NewNormalError(NormalSubcategoryTransaction, 32, http.StatusBadRequest, "import file encoding not supported")
|
||||||
ErrImportFileColumnMappingInvalid = NewSystemError(NormalSubcategoryTransaction, 33, http.StatusBadRequest, "column mapping invalid")
|
ErrImportFileColumnMappingInvalid = NewNormalError(NormalSubcategoryTransaction, 33, http.StatusBadRequest, "column mapping invalid")
|
||||||
ErrImportFileTransactionTypeMappingInvalid = NewSystemError(NormalSubcategoryTransaction, 34, http.StatusBadRequest, "transaction type mapping invalid")
|
ErrImportFileTransactionTypeMappingInvalid = NewNormalError(NormalSubcategoryTransaction, 34, http.StatusBadRequest, "transaction type mapping invalid")
|
||||||
ErrImportFileTransactionTimeFormatInvalid = NewSystemError(NormalSubcategoryTransaction, 35, http.StatusBadRequest, "transaction time format invalid")
|
ErrImportFileTransactionTimeFormatInvalid = NewNormalError(NormalSubcategoryTransaction, 35, http.StatusBadRequest, "transaction time format invalid")
|
||||||
ErrImportFileTransactionTimezoneFormatInvalid = NewSystemError(NormalSubcategoryTransaction, 36, http.StatusBadRequest, "transaction time zone format invalid")
|
ErrImportFileTransactionTimezoneFormatInvalid = NewNormalError(NormalSubcategoryTransaction, 36, http.StatusBadRequest, "transaction time zone format invalid")
|
||||||
|
ErrCannotMoveTransactionToSameAccount = NewNormalError(NormalSubcategoryTransaction, 37, http.StatusBadRequest, "cannot move transaction to same account")
|
||||||
|
ErrCannotMoveTransactionFromOrToHiddenAccount = NewNormalError(NormalSubcategoryTransaction, 38, http.StatusBadRequest, "cannot move transaction from or to hidden account")
|
||||||
|
ErrCannotMoveTransactionFromOrToParentAccount = NewNormalError(NormalSubcategoryTransaction, 39, http.StatusBadRequest, "cannot move transaction from or to parent account")
|
||||||
|
ErrCannotMoveTransactionBetweenAccountsWithDifferentCurrencies = NewNormalError(NormalSubcategoryTransaction, 40, http.StatusBadRequest, "cannot move transaction between accounts with different currencies")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ var (
|
|||||||
ErrTwoFactorRecoveryCodeNotExist = NewNormalError(NormalSubcategoryTwofactor, 2, http.StatusUnauthorized, "two-factor backup code does not exist")
|
ErrTwoFactorRecoveryCodeNotExist = NewNormalError(NormalSubcategoryTwofactor, 2, http.StatusUnauthorized, "two-factor backup code does not exist")
|
||||||
ErrTwoFactorIsNotEnabled = NewNormalError(NormalSubcategoryTwofactor, 3, http.StatusBadRequest, "two-factor is not enabled")
|
ErrTwoFactorIsNotEnabled = NewNormalError(NormalSubcategoryTwofactor, 3, http.StatusBadRequest, "two-factor is not enabled")
|
||||||
ErrTwoFactorAlreadyEnabled = NewNormalError(NormalSubcategoryTwofactor, 4, http.StatusBadRequest, "two-factor has already been enabled")
|
ErrTwoFactorAlreadyEnabled = NewNormalError(NormalSubcategoryTwofactor, 4, http.StatusBadRequest, "two-factor has already been enabled")
|
||||||
|
ErrPasscodeEmpty = NewNormalError(NormalSubcategoryTwofactor, 5, http.StatusUnauthorized, "passcode is empty")
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-1
@@ -23,7 +23,7 @@ var (
|
|||||||
ErrUserRegistrationNotAllowed = NewNormalError(NormalSubcategoryUser, 14, http.StatusBadRequest, "user registration not allowed")
|
ErrUserRegistrationNotAllowed = NewNormalError(NormalSubcategoryUser, 14, http.StatusBadRequest, "user registration not allowed")
|
||||||
ErrUserDefaultAccountIsInvalid = NewNormalError(NormalSubcategoryUser, 15, http.StatusBadRequest, "user default account is invalid")
|
ErrUserDefaultAccountIsInvalid = NewNormalError(NormalSubcategoryUser, 15, http.StatusBadRequest, "user default account is invalid")
|
||||||
ErrUserIsDisabled = NewNormalError(NormalSubcategoryUser, 16, http.StatusBadRequest, "user is disabled")
|
ErrUserIsDisabled = NewNormalError(NormalSubcategoryUser, 16, http.StatusBadRequest, "user is disabled")
|
||||||
ErrEmptyIsInvalid = NewNormalError(NormalSubcategoryUser, 17, http.StatusBadRequest, "email is invalid")
|
ErrEmailIsInvalid = NewNormalError(NormalSubcategoryUser, 17, http.StatusBadRequest, "email is invalid")
|
||||||
ErrEmailIsEmptyOrInvalid = NewNormalError(NormalSubcategoryUser, 18, http.StatusBadRequest, "email is empty or invalid")
|
ErrEmailIsEmptyOrInvalid = NewNormalError(NormalSubcategoryUser, 18, http.StatusBadRequest, "email is empty or invalid")
|
||||||
ErrNewPasswordEqualsOldInvalid = NewNormalError(NormalSubcategoryUser, 19, http.StatusBadRequest, "new password equals old password")
|
ErrNewPasswordEqualsOldInvalid = NewNormalError(NormalSubcategoryUser, 19, http.StatusBadRequest, "new password equals old password")
|
||||||
ErrEmailIsNotVerified = NewNormalError(NormalSubcategoryUser, 20, http.StatusBadRequest, "email is not verified")
|
ErrEmailIsNotVerified = NewNormalError(NormalSubcategoryUser, 20, http.StatusBadRequest, "email is not verified")
|
||||||
@@ -38,4 +38,7 @@ var (
|
|||||||
ErrUserAvatarExtensionInvalid = NewNormalError(NormalSubcategoryUser, 29, http.StatusNotFound, "user avatar file extension invalid")
|
ErrUserAvatarExtensionInvalid = NewNormalError(NormalSubcategoryUser, 29, http.StatusNotFound, "user avatar file extension invalid")
|
||||||
ErrExceedMaxUserAvatarFileSize = NewNormalError(NormalSubcategoryUser, 30, http.StatusBadRequest, "exceed the maximum size of user avatar file")
|
ErrExceedMaxUserAvatarFileSize = NewNormalError(NormalSubcategoryUser, 30, http.StatusBadRequest, "exceed the maximum size of user avatar file")
|
||||||
ErrNotPermittedToPerformThisAction = NewNormalError(NormalSubcategoryUser, 31, http.StatusBadRequest, "not permitted to perform this action")
|
ErrNotPermittedToPerformThisAction = NewNormalError(NormalSubcategoryUser, 31, http.StatusBadRequest, "not permitted to perform this action")
|
||||||
|
ErrCannotLoginByPassword = NewNormalError(NormalSubcategoryUser, 32, http.StatusBadRequest, "cannot login by password")
|
||||||
|
ErrUserNameIsInvalid = NewNormalError(NormalSubcategoryUser, 33, http.StatusBadRequest, "user name is invalid")
|
||||||
|
ErrNickNameIsInvalid = NewNormalError(NormalSubcategoryUser, 34, http.StatusBadRequest, "nick name is invalid")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package exchangerates
|
package exchangerates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
@@ -28,23 +26,10 @@ type HttpExchangeRatesDataSource interface {
|
|||||||
type CommonHttpExchangeRatesDataProvider struct {
|
type CommonHttpExchangeRatesDataProvider struct {
|
||||||
ExchangeRatesDataProvider
|
ExchangeRatesDataProvider
|
||||||
dataSource HttpExchangeRatesDataSource
|
dataSource HttpExchangeRatesDataSource
|
||||||
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *CommonHttpExchangeRatesDataProvider) GetLatestExchangeRates(c core.Context, uid int64, currentConfig *settings.Config) (*models.LatestExchangeRateResponse, error) {
|
func (e *CommonHttpExchangeRatesDataProvider) GetLatestExchangeRates(c core.Context, uid int64, currentConfig *settings.Config) (*models.LatestExchangeRateResponse, error) {
|
||||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
|
||||||
utils.SetProxyUrl(transport, currentConfig.ExchangeRatesProxy)
|
|
||||||
|
|
||||||
if currentConfig.ExchangeRatesSkipTLSVerify {
|
|
||||||
transport.TLSClientConfig = &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
Timeout: time.Duration(currentConfig.ExchangeRatesRequestTimeout) * time.Millisecond,
|
|
||||||
}
|
|
||||||
|
|
||||||
requests, err := e.dataSource.BuildRequests()
|
requests, err := e.dataSource.BuildRequests()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -56,14 +41,7 @@ func (e *CommonHttpExchangeRatesDataProvider) GetLatestExchangeRates(c core.Cont
|
|||||||
|
|
||||||
for i := 0; i < len(requests); i++ {
|
for i := 0; i < len(requests); i++ {
|
||||||
req := requests[i]
|
req := requests[i]
|
||||||
|
resp, err := e.httpClient.Do(req)
|
||||||
if len(req.Header.Values("User-Agent")) < 1 {
|
|
||||||
req.Header.Set("User-Agent", settings.GetUserAgent())
|
|
||||||
} else if req.Header.Get("User-Agent") == "" {
|
|
||||||
req.Header.Del("User-Agent")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(c, "[common_http_exchange_rates_data_provider.GetLatestExchangeRates] failed to request latest exchange rate data for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[common_http_exchange_rates_data_provider.GetLatestExchangeRates] failed to request latest exchange rate data for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
@@ -76,7 +54,7 @@ func (e *CommonHttpExchangeRatesDataProvider) GetLatestExchangeRates(c core.Cont
|
|||||||
log.Debugf(c, "[common_http_exchange_rates_data_provider.GetLatestExchangeRates] response#%d is %s", i, body)
|
log.Debugf(c, "[common_http_exchange_rates_data_provider.GetLatestExchangeRates] response#%d is %s", i, body)
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
log.Errorf(c, "[common_http_exchange_rates_data_provider.GetLatestExchangeRates] failed to get latest exchange rate data response for user \"uid:%d\", because response code is not %d", uid, resp.StatusCode)
|
log.Errorf(c, "[common_http_exchange_rates_data_provider.GetLatestExchangeRates] failed to get latest exchange rate data response for user \"uid:%d\", because response code is %d", uid, resp.StatusCode)
|
||||||
return nil, errs.ErrFailedToRequestRemoteApi
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,8 +103,9 @@ func (e *CommonHttpExchangeRatesDataProvider) GetLatestExchangeRates(c core.Cont
|
|||||||
return finalExchangeRateResponse, nil
|
return finalExchangeRateResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCommonHttpExchangeRatesDataProvider(dataSource HttpExchangeRatesDataSource) *CommonHttpExchangeRatesDataProvider {
|
func newCommonHttpExchangeRatesDataProvider(config *settings.Config, dataSource HttpExchangeRatesDataSource) *CommonHttpExchangeRatesDataProvider {
|
||||||
return &CommonHttpExchangeRatesDataProvider{
|
return &CommonHttpExchangeRatesDataProvider{
|
||||||
dataSource: dataSource,
|
dataSource: dataSource,
|
||||||
|
httpClient: utils.NewHttpClient(config.ExchangeRatesRequestTimeout, config.ExchangeRatesProxy, config.ExchangeRatesSkipTLSVerify, settings.GetUserAgent()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,55 +20,55 @@ var (
|
|||||||
// InitializeExchangeRatesDataSource initializes the current exchange rates data source according to the config
|
// InitializeExchangeRatesDataSource initializes the current exchange rates data source according to the config
|
||||||
func InitializeExchangeRatesDataSource(config *settings.Config) error {
|
func InitializeExchangeRatesDataSource(config *settings.Config) error {
|
||||||
if config.ExchangeRatesDataSource == settings.ReserveBankOfAustraliaDataSource {
|
if config.ExchangeRatesDataSource == settings.ReserveBankOfAustraliaDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&ReserveBankOfAustraliaDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &ReserveBankOfAustraliaDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.BankOfCanadaDataSource {
|
} else if config.ExchangeRatesDataSource == settings.BankOfCanadaDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&BankOfCanadaDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &BankOfCanadaDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.CzechNationalBankDataSource {
|
} else if config.ExchangeRatesDataSource == settings.CzechNationalBankDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&CzechNationalBankDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &CzechNationalBankDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.DanmarksNationalbankDataSource {
|
} else if config.ExchangeRatesDataSource == settings.DanmarksNationalbankDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&DanmarksNationalbankDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &DanmarksNationalbankDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.EuroCentralBankDataSource {
|
} else if config.ExchangeRatesDataSource == settings.EuroCentralBankDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&EuroCentralBankDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &EuroCentralBankDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.NationalBankOfGeorgiaDataSource {
|
} else if config.ExchangeRatesDataSource == settings.NationalBankOfGeorgiaDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&NationalBankOfGeorgiaDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &NationalBankOfGeorgiaDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.CentralBankOfHungaryDataSource {
|
} else if config.ExchangeRatesDataSource == settings.CentralBankOfHungaryDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&CentralBankOfHungaryDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &CentralBankOfHungaryDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.BankOfIsraelDataSource {
|
} else if config.ExchangeRatesDataSource == settings.BankOfIsraelDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&BankOfIsraelDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &BankOfIsraelDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.CentralBankOfMyanmarDataSource {
|
} else if config.ExchangeRatesDataSource == settings.CentralBankOfMyanmarDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&CentralBankOfMyanmarDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &CentralBankOfMyanmarDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.NorgesBankDataSource {
|
} else if config.ExchangeRatesDataSource == settings.NorgesBankDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&NorgesBankDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &NorgesBankDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.NationalBankOfPolandDataSource {
|
} else if config.ExchangeRatesDataSource == settings.NationalBankOfPolandDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&NationalBankOfPolandDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &NationalBankOfPolandDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.NationalBankOfRomaniaDataSource {
|
} else if config.ExchangeRatesDataSource == settings.NationalBankOfRomaniaDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&NationalBankOfRomaniaDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &NationalBankOfRomaniaDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.BankOfRussiaDataSource {
|
} else if config.ExchangeRatesDataSource == settings.BankOfRussiaDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&BankOfRussiaDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &BankOfRussiaDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.SwissNationalBankDataSource {
|
} else if config.ExchangeRatesDataSource == settings.SwissNationalBankDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&SwissNationalBankDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &SwissNationalBankDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.NationalBankOfUkraineDataSource {
|
} else if config.ExchangeRatesDataSource == settings.NationalBankOfUkraineDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&NationalBankOfUkraineDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &NationalBankOfUkraineDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.CentralBankOfUzbekistanDataSource {
|
} else if config.ExchangeRatesDataSource == settings.CentralBankOfUzbekistanDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&CentralBankOfUzbekistanDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &CentralBankOfUzbekistanDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.InternationalMonetaryFundDataSource {
|
} else if config.ExchangeRatesDataSource == settings.InternationalMonetaryFundDataSource {
|
||||||
Container.current = newCommonHttpExchangeRatesDataProvider(&InternationalMonetaryFundDataSource{})
|
Container.current = newCommonHttpExchangeRatesDataProvider(config, &InternationalMonetaryFundDataSource{})
|
||||||
return nil
|
return nil
|
||||||
} else if config.ExchangeRatesDataSource == settings.UserCustomExchangeRatesDataSource {
|
} else if config.ExchangeRatesDataSource == settings.UserCustomExchangeRatesDataSource {
|
||||||
Container.current = newUserCustomExchangeRatesDataProvider()
|
Container.current = newUserCustomExchangeRatesDataProvider()
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
@@ -28,7 +26,8 @@ type HttpLargeLanguageModelAdapter interface {
|
|||||||
// CommonHttpLargeLanguageModelProvider defines the structure of common http large language model provider
|
// CommonHttpLargeLanguageModelProvider defines the structure of common http large language model provider
|
||||||
type CommonHttpLargeLanguageModelProvider struct {
|
type CommonHttpLargeLanguageModelProvider struct {
|
||||||
provider.LargeLanguageModelProvider
|
provider.LargeLanguageModelProvider
|
||||||
adapter HttpLargeLanguageModelAdapter
|
adapter HttpLargeLanguageModelAdapter
|
||||||
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetJsonResponse returns the json response from common http large language model provider
|
// GetJsonResponse returns the json response from common http large language model provider
|
||||||
@@ -51,20 +50,6 @@ func (p *CommonHttpLargeLanguageModelProvider) GetJsonResponse(c core.Context, u
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *CommonHttpLargeLanguageModelProvider) getTextualResponse(c core.Context, uid int64, currentLLMConfig *settings.LLMConfig, request *data.LargeLanguageModelRequest, responseType data.LargeLanguageModelResponseFormat) (*data.LargeLanguageModelTextualResponse, error) {
|
func (p *CommonHttpLargeLanguageModelProvider) getTextualResponse(c core.Context, uid int64, currentLLMConfig *settings.LLMConfig, request *data.LargeLanguageModelRequest, responseType data.LargeLanguageModelResponseFormat) (*data.LargeLanguageModelTextualResponse, error) {
|
||||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
|
||||||
utils.SetProxyUrl(transport, currentLLMConfig.LargeLanguageModelAPIProxy)
|
|
||||||
|
|
||||||
if currentLLMConfig.LargeLanguageModelAPISkipTLSVerify {
|
|
||||||
transport.TLSClientConfig = &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
Timeout: time.Duration(currentLLMConfig.LargeLanguageModelAPIRequestTimeout) * time.Millisecond,
|
|
||||||
}
|
|
||||||
|
|
||||||
httpRequest, err := p.adapter.BuildTextualRequest(c, uid, request, responseType)
|
httpRequest, err := p.adapter.BuildTextualRequest(c, uid, request, responseType)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -72,9 +57,7 @@ func (p *CommonHttpLargeLanguageModelProvider) getTextualResponse(c core.Context
|
|||||||
return nil, errs.ErrFailedToRequestRemoteApi
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
}
|
}
|
||||||
|
|
||||||
httpRequest.Header.Set("User-Agent", settings.GetUserAgent())
|
resp, err := p.httpClient.Do(httpRequest)
|
||||||
|
|
||||||
resp, err := client.Do(httpRequest)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(c, "[common_http_large_language_model_provider.getTextualResponse] failed to request large language model api for user \"uid:%d\", because %s", uid, err.Error())
|
log.Errorf(c, "[common_http_large_language_model_provider.getTextualResponse] failed to request large language model api for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
@@ -95,8 +78,9 @@ func (p *CommonHttpLargeLanguageModelProvider) getTextualResponse(c core.Context
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewCommonHttpLargeLanguageModelProvider creates a http adapter based large language model provider instance
|
// NewCommonHttpLargeLanguageModelProvider creates a http adapter based large language model provider instance
|
||||||
func NewCommonHttpLargeLanguageModelProvider(adapter HttpLargeLanguageModelAdapter) *CommonHttpLargeLanguageModelProvider {
|
func NewCommonHttpLargeLanguageModelProvider(llmConfig *settings.LLMConfig, adapter HttpLargeLanguageModelAdapter) *CommonHttpLargeLanguageModelProvider {
|
||||||
return &CommonHttpLargeLanguageModelProvider{
|
return &CommonHttpLargeLanguageModelProvider{
|
||||||
adapter: adapter,
|
adapter: adapter,
|
||||||
|
httpClient: utils.NewHttpClient(llmConfig.LargeLanguageModelAPIRequestTimeout, llmConfig.LargeLanguageModelAPIProxy, llmConfig.LargeLanguageModelAPISkipTLSVerify, settings.GetUserAgent()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ func (p *GoogleAILargeLanguageModelAdapter) buildJsonRequestBody(c core.Context,
|
|||||||
|
|
||||||
// NewGoogleAILargeLanguageModelProvider creates a new Google AI large language model provider instance
|
// NewGoogleAILargeLanguageModelProvider creates a new Google AI large language model provider instance
|
||||||
func NewGoogleAILargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
|
func NewGoogleAILargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
|
||||||
return common.NewCommonHttpLargeLanguageModelProvider(&GoogleAILargeLanguageModelAdapter{
|
return common.NewCommonHttpLargeLanguageModelProvider(llmConfig, &GoogleAILargeLanguageModelAdapter{
|
||||||
GoogleAIAPIKey: llmConfig.GoogleAIAPIKey,
|
GoogleAIAPIKey: llmConfig.GoogleAIAPIKey,
|
||||||
GoogleAIModelID: llmConfig.GoogleAIModelID,
|
GoogleAIModelID: llmConfig.GoogleAIModelID,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ func (p *OllamaLargeLanguageModelAdapter) getOllamaRequestUrl() string {
|
|||||||
|
|
||||||
// NewOllamaLargeLanguageModelProvider creates a new Ollama large language model provider instance
|
// NewOllamaLargeLanguageModelProvider creates a new Ollama large language model provider instance
|
||||||
func NewOllamaLargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
|
func NewOllamaLargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
|
||||||
return common.NewCommonHttpLargeLanguageModelProvider(&OllamaLargeLanguageModelAdapter{
|
return common.NewCommonHttpLargeLanguageModelProvider(llmConfig, &OllamaLargeLanguageModelAdapter{
|
||||||
OllamaServerURL: llmConfig.OllamaServerURL,
|
OllamaServerURL: llmConfig.OllamaServerURL,
|
||||||
OllamaModelID: llmConfig.OllamaModelID,
|
OllamaModelID: llmConfig.OllamaModelID,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func (p *OpenAIOfficialChatCompletionsAPIProvider) GetModelID() string {
|
|||||||
|
|
||||||
// NewOpenAILargeLanguageModelProvider creates a new OpenAI large language model provider instance
|
// NewOpenAILargeLanguageModelProvider creates a new OpenAI large language model provider instance
|
||||||
func NewOpenAILargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
|
func NewOpenAILargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
|
||||||
return newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(&OpenAIOfficialChatCompletionsAPIProvider{
|
return newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(llmConfig, &OpenAIOfficialChatCompletionsAPIProvider{
|
||||||
OpenAIAPIKey: llmConfig.OpenAIAPIKey,
|
OpenAIAPIKey: llmConfig.OpenAIAPIKey,
|
||||||
OpenAIModelID: llmConfig.OpenAIModelID,
|
OpenAIModelID: llmConfig.OpenAIModelID,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/llm/provider"
|
"github.com/mayswind/ezbookkeeping/pkg/llm/provider"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/llm/provider/common"
|
"github.com/mayswind/ezbookkeeping/pkg/llm/provider/common"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OpenAIChatCompletionsAPIProvider defines the structure of OpenAI chat completions API provider
|
// OpenAIChatCompletionsAPIProvider defines the structure of OpenAI chat completions API provider
|
||||||
@@ -212,8 +213,8 @@ func (p *CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter) buildJsonReque
|
|||||||
return requestBodyBytes, nil
|
return requestBodyBytes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(apiProvider OpenAIChatCompletionsAPIProvider) provider.LargeLanguageModelProvider {
|
func newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(llmConfig *settings.LLMConfig, apiProvider OpenAIChatCompletionsAPIProvider) provider.LargeLanguageModelProvider {
|
||||||
return common.NewCommonHttpLargeLanguageModelProvider(&CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter{
|
return common.NewCommonHttpLargeLanguageModelProvider(llmConfig, &CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter{
|
||||||
apiProvider: apiProvider,
|
apiProvider: apiProvider,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func (p *OpenAICompatibleChatCompletionsAPIProvider) getFinalChatCompletionsRequ
|
|||||||
|
|
||||||
// NewOpenAICompatibleLargeLanguageModelProvider creates a new OpenAI compatible large language model provider instance
|
// NewOpenAICompatibleLargeLanguageModelProvider creates a new OpenAI compatible large language model provider instance
|
||||||
func NewOpenAICompatibleLargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
|
func NewOpenAICompatibleLargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
|
||||||
return newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(&OpenAICompatibleChatCompletionsAPIProvider{
|
return newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(llmConfig, &OpenAICompatibleChatCompletionsAPIProvider{
|
||||||
OpenAICompatibleBaseURL: llmConfig.OpenAICompatibleBaseURL,
|
OpenAICompatibleBaseURL: llmConfig.OpenAICompatibleBaseURL,
|
||||||
OpenAICompatibleAPIKey: llmConfig.OpenAICompatibleAPIKey,
|
OpenAICompatibleAPIKey: llmConfig.OpenAICompatibleAPIKey,
|
||||||
OpenAICompatibleModelID: llmConfig.OpenAICompatibleModelID,
|
OpenAICompatibleModelID: llmConfig.OpenAICompatibleModelID,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func (p *OpenRouterChatCompletionsAPIProvider) GetModelID() string {
|
|||||||
|
|
||||||
// NewOpenRouterLargeLanguageModelProvider creates a new OpenRouter large language model provider instance
|
// NewOpenRouterLargeLanguageModelProvider creates a new OpenRouter large language model provider instance
|
||||||
func NewOpenRouterLargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
|
func NewOpenRouterLargeLanguageModelProvider(llmConfig *settings.LLMConfig) provider.LargeLanguageModelProvider {
|
||||||
return newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(&OpenRouterChatCompletionsAPIProvider{
|
return newCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter(llmConfig, &OpenRouterChatCompletionsAPIProvider{
|
||||||
OpenRouterAPIKey: llmConfig.OpenRouterAPIKey,
|
OpenRouterAPIKey: llmConfig.OpenRouterAPIKey,
|
||||||
OpenRouterModelID: llmConfig.OpenRouterModelID,
|
OpenRouterModelID: llmConfig.OpenRouterModelID,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ var AllLanguages = map[string]*LocaleInfo{
|
|||||||
"ja": {
|
"ja": {
|
||||||
Content: ja,
|
Content: ja,
|
||||||
},
|
},
|
||||||
|
"ko": {
|
||||||
|
Content: ko,
|
||||||
|
},
|
||||||
"nl": {
|
"nl": {
|
||||||
Content: nl,
|
Content: nl,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package locales
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ko = &LocaleTextItems{
|
||||||
|
DefaultTypes: &DefaultTypes{
|
||||||
|
DecimalSeparator: core.DECIMAL_SEPARATOR_DOT,
|
||||||
|
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_COMMA,
|
||||||
|
},
|
||||||
|
DataConverterTextItems: &DataConverterTextItems{
|
||||||
|
Alipay: "Alipay",
|
||||||
|
WeChatWallet: "Wallet",
|
||||||
|
},
|
||||||
|
VerifyEmailTextItems: &VerifyEmailTextItems{
|
||||||
|
Title: "이메일 인증",
|
||||||
|
SalutationFormat: "안녕하세요 %s님,",
|
||||||
|
DescriptionAboveBtn: "이메일 주소를 확인하려면 아래 링크를 클릭해주세요.",
|
||||||
|
VerifyEmail: "이메일 인증",
|
||||||
|
DescriptionBelowBtnFormat: "%s 계정에 가입하지 않으셨다면 이 이메일을 무시해주세요. 위 링크를 클릭할 수 없는 경우, 위 URL을 복사하여 브라우저에 붙여넣어 주세요. 이메일 인증 링크는 %v분 후에 만료됩니다.",
|
||||||
|
},
|
||||||
|
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
|
||||||
|
Title: "비밀번호 재설정",
|
||||||
|
SalutationFormat: "안녕하세요 %s님,",
|
||||||
|
DescriptionAboveBtn: "비밀번호 재설정 요청이 있었습니다. 아래 링크를 클릭하시면 비밀번호를 재설정할 수 있습니다.",
|
||||||
|
ResetPassword: "비밀번호 재설정",
|
||||||
|
DescriptionBelowBtnFormat: "비밀번호 재설정을 요청하지 않으셨다면 이 이메일을 무시해주세요. 위 링크를 클릭할 수 없는 경우, 위 URL을 복사하여 브라우저에 붙여넣어 주세요. 비밀번호 재설정 링크는 %v분 후에 만료됩니다.",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -73,6 +73,7 @@ func InitializeMCPHandlers(config *settings.Config) error {
|
|||||||
registerMCPTextContentToolHandler(container, MCPAddTransactionToolHandler)
|
registerMCPTextContentToolHandler(container, MCPAddTransactionToolHandler)
|
||||||
registerMCPTextContentToolHandler(container, MCPQueryTransactionsToolHandler)
|
registerMCPTextContentToolHandler(container, MCPQueryTransactionsToolHandler)
|
||||||
registerMCPTextContentToolHandler(container, MCPQueryAllAccountsToolHandler)
|
registerMCPTextContentToolHandler(container, MCPQueryAllAccountsToolHandler)
|
||||||
|
registerMCPTextContentToolHandler(container, MCPQueryAllAccountsBalanceToolHandler)
|
||||||
registerMCPTextContentToolHandler(container, MCPQueryAllTransactionCategoriesToolHandler)
|
registerMCPTextContentToolHandler(container, MCPQueryAllTransactionCategoriesToolHandler)
|
||||||
registerMCPTextContentToolHandler(container, MCPQueryAllTransactionTagsToolHandler)
|
registerMCPTextContentToolHandler(container, MCPQueryAllTransactionTagsToolHandler)
|
||||||
registerMCPTextContentToolHandler(container, MCPQueryLatestExchangeRatesToolHandler)
|
registerMCPTextContentToolHandler(container, MCPQueryLatestExchangeRatesToolHandler)
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package mcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MCPQueryAllAccountsBalanceResponse represents the response structure for querying accounts balance
|
||||||
|
type MCPQueryAllAccountsBalanceResponse struct {
|
||||||
|
CashAccounts []*MCPAccountBalanceInfo `json:"cashAccounts,omitempty" jsonschema_description:"List of cash account balances"`
|
||||||
|
CheckingAccounts []*MCPAccountBalanceInfo `json:"checkingAccounts,omitempty" jsonschema_description:"List of checking account balances"`
|
||||||
|
SavingsAccounts []*MCPAccountBalanceInfo `json:"savingsAccounts,omitempty" jsonschema_description:"List of savings account balances"`
|
||||||
|
CreditCardAccounts []*MCPAccountBalanceInfo `json:"creditCardAccounts,omitempty" jsonschema_description:"List of credit card account outstanding balances"`
|
||||||
|
VirtualAccounts []*MCPAccountBalanceInfo `json:"virtualAccounts,omitempty" jsonschema_description:"List of virtual account balances"`
|
||||||
|
DebtAccounts []*MCPAccountBalanceInfo `json:"debtAccounts,omitempty" jsonschema_description:"List of debt account outstanding balances"`
|
||||||
|
ReceivableAccounts []*MCPAccountBalanceInfo `json:"receivableAccounts,omitempty" jsonschema_description:"List of receivable account balances"`
|
||||||
|
CertificateOfDepositAccounts []*MCPAccountBalanceInfo `json:"certificateOfDepositAccounts,omitempty" jsonschema_description:"List of certificate of deposit account balances"`
|
||||||
|
InvestmentAccounts []*MCPAccountBalanceInfo `json:"investmentAccounts,omitempty" jsonschema_description:"List of investment account balances"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCPAccountBalanceInfo defines the structure of account balance information
|
||||||
|
type MCPAccountBalanceInfo struct {
|
||||||
|
Name string `json:"name" jsonschema_description:"Account name"`
|
||||||
|
Type string `json:"type" jsonschema:"enum=asset,enum=liability" jsonschema_description:"Account type (asset or liability)"`
|
||||||
|
Balance string `json:"balance,omitempty" jsonschema_description:"Current balance of the account"`
|
||||||
|
OutstandingBalance string `json:"outstandingBalance,omitempty" jsonschema_description:"Current outstanding balance of the account (positive value indicates amount owed)"`
|
||||||
|
Currency string `json:"currency" jsonschema_description:"Currency code of the account (e.g. USD, EUR)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type mcpQueryAllAccountsBalanceToolHandler struct{}
|
||||||
|
|
||||||
|
var MCPQueryAllAccountsBalanceToolHandler = &mcpQueryAllAccountsBalanceToolHandler{}
|
||||||
|
|
||||||
|
// Name returns the name of the MCP tool
|
||||||
|
func (h *mcpQueryAllAccountsBalanceToolHandler) Name() string {
|
||||||
|
return "query_all_accounts_balance"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description of the MCP tool
|
||||||
|
func (h *mcpQueryAllAccountsBalanceToolHandler) Description() string {
|
||||||
|
return "Query all accounts balance for the current user in ezBookkeeping."
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputType returns the input type for the MCP tool request
|
||||||
|
func (h *mcpQueryAllAccountsBalanceToolHandler) InputType() reflect.Type {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutputType returns the output type for the MCP tool response
|
||||||
|
func (h *mcpQueryAllAccountsBalanceToolHandler) OutputType() reflect.Type {
|
||||||
|
return reflect.TypeOf(&MCPQueryAllAccountsBalanceResponse{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle processes the MCP call tool request and returns the response
|
||||||
|
func (h *mcpQueryAllAccountsBalanceToolHandler) Handle(c *core.WebContext, callToolReq *MCPCallToolRequest, user *models.User, currentConfig *settings.Config, services MCPAvailableServices) (any, []*MCPTextContent, error) {
|
||||||
|
uid := user.Uid
|
||||||
|
accounts, err := services.GetAccountService().GetAllAccountsByUid(c, uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[query_all_accounts_balance_tool_handler.Handle] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
structuredResponse, response, err := h.createNewMCPQueryAllAccountsBalanceResponse(c, accounts)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return structuredResponse, response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *mcpQueryAllAccountsBalanceToolHandler) createNewMCPQueryAllAccountsBalanceResponse(c *core.WebContext, accounts []*models.Account) (any, []*MCPTextContent, error) {
|
||||||
|
response := MCPQueryAllAccountsBalanceResponse{}
|
||||||
|
|
||||||
|
for i := 0; i < len(accounts); i++ {
|
||||||
|
account := accounts[i]
|
||||||
|
|
||||||
|
if account.Hidden || (account.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS && account.ParentAccountId == models.LevelOneAccountParentId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.Category == models.ACCOUNT_CATEGORY_CASH {
|
||||||
|
if response.CashAccounts == nil {
|
||||||
|
response.CashAccounts = make([]*MCPAccountBalanceInfo, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.CashAccounts = append(response.CashAccounts, h.createNewMCPAccountBalanceInfo(account))
|
||||||
|
} else if account.Category == models.ACCOUNT_CATEGORY_CHECKING_ACCOUNT {
|
||||||
|
if response.CheckingAccounts == nil {
|
||||||
|
response.CheckingAccounts = make([]*MCPAccountBalanceInfo, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.CheckingAccounts = append(response.CheckingAccounts, h.createNewMCPAccountBalanceInfo(account))
|
||||||
|
} else if account.Category == models.ACCOUNT_CATEGORY_SAVINGS_ACCOUNT {
|
||||||
|
if response.SavingsAccounts == nil {
|
||||||
|
response.SavingsAccounts = make([]*MCPAccountBalanceInfo, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.SavingsAccounts = append(response.SavingsAccounts, h.createNewMCPAccountBalanceInfo(account))
|
||||||
|
} else if account.Category == models.ACCOUNT_CATEGORY_CREDIT_CARD {
|
||||||
|
if response.CreditCardAccounts == nil {
|
||||||
|
response.CreditCardAccounts = make([]*MCPAccountBalanceInfo, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.CreditCardAccounts = append(response.CreditCardAccounts, h.createNewMCPAccountBalanceInfo(account))
|
||||||
|
} else if account.Category == models.ACCOUNT_CATEGORY_VIRTUAL {
|
||||||
|
if response.VirtualAccounts == nil {
|
||||||
|
response.VirtualAccounts = make([]*MCPAccountBalanceInfo, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.VirtualAccounts = append(response.VirtualAccounts, h.createNewMCPAccountBalanceInfo(account))
|
||||||
|
} else if account.Category == models.ACCOUNT_CATEGORY_DEBT {
|
||||||
|
if response.DebtAccounts == nil {
|
||||||
|
response.DebtAccounts = make([]*MCPAccountBalanceInfo, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.DebtAccounts = append(response.DebtAccounts, h.createNewMCPAccountBalanceInfo(account))
|
||||||
|
} else if account.Category == models.ACCOUNT_CATEGORY_RECEIVABLES {
|
||||||
|
if response.ReceivableAccounts == nil {
|
||||||
|
response.ReceivableAccounts = make([]*MCPAccountBalanceInfo, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.ReceivableAccounts = append(response.ReceivableAccounts, h.createNewMCPAccountBalanceInfo(account))
|
||||||
|
} else if account.Category == models.ACCOUNT_CATEGORY_CERTIFICATE_OF_DEPOSIT {
|
||||||
|
if response.CertificateOfDepositAccounts == nil {
|
||||||
|
response.CertificateOfDepositAccounts = make([]*MCPAccountBalanceInfo, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.CertificateOfDepositAccounts = append(response.CertificateOfDepositAccounts, h.createNewMCPAccountBalanceInfo(account))
|
||||||
|
} else if account.Category == models.ACCOUNT_CATEGORY_INVESTMENT {
|
||||||
|
if response.InvestmentAccounts == nil {
|
||||||
|
response.InvestmentAccounts = make([]*MCPAccountBalanceInfo, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.InvestmentAccounts = append(response.InvestmentAccounts, h.createNewMCPAccountBalanceInfo(account))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := json.Marshal(response)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, []*MCPTextContent{
|
||||||
|
NewMCPTextContent(string(content)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *mcpQueryAllAccountsBalanceToolHandler) createNewMCPAccountBalanceInfo(account *models.Account) *MCPAccountBalanceInfo {
|
||||||
|
accountResp := account.ToAccountInfoResponse()
|
||||||
|
|
||||||
|
balanceInfo := &MCPAccountBalanceInfo{
|
||||||
|
Name: accountResp.Name,
|
||||||
|
Currency: accountResp.Currency,
|
||||||
|
}
|
||||||
|
|
||||||
|
if accountResp.IsAsset {
|
||||||
|
balanceInfo.Type = "asset"
|
||||||
|
balanceInfo.Balance = utils.FormatAmount(accountResp.Balance)
|
||||||
|
} else if accountResp.IsLiability {
|
||||||
|
balanceInfo.Type = "liability"
|
||||||
|
balanceInfo.OutstandingBalance = utils.FormatAmount(-accountResp.Balance)
|
||||||
|
}
|
||||||
|
|
||||||
|
return balanceInfo
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,142 +22,185 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// JWTAuthorization verifies whether current request is valid by jwt token in header
|
// JWTAuthorization verifies whether current request is valid by jwt token in header
|
||||||
func JWTAuthorization(c *core.WebContext) {
|
func JWTAuthorization(config *settings.Config) core.MiddlewareHandlerFunc {
|
||||||
jwtAuthorization(c, TOKEN_SOURCE_TYPE_HEADER)
|
return jwtAuthorization(config, TOKEN_SOURCE_TYPE_HEADER)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWTAuthorizationByQueryString verifies whether current request is valid by jwt token in query string
|
// JWTAuthorizationByQueryString verifies whether current request is valid by jwt token in query string
|
||||||
func JWTAuthorizationByQueryString(c *core.WebContext) {
|
func JWTAuthorizationByQueryString(config *settings.Config) core.MiddlewareHandlerFunc {
|
||||||
jwtAuthorization(c, TOKEN_SOURCE_TYPE_ARGUMENT)
|
return jwtAuthorization(config, TOKEN_SOURCE_TYPE_ARGUMENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWTAuthorizationByCookie verifies whether current request is valid by jwt token in cookie
|
// JWTAuthorizationByCookie verifies whether current request is valid by jwt token in cookie
|
||||||
func JWTAuthorizationByCookie(c *core.WebContext) {
|
func JWTAuthorizationByCookie(config *settings.Config) core.MiddlewareHandlerFunc {
|
||||||
jwtAuthorization(c, TOKEN_SOURCE_TYPE_COOKIE)
|
return jwtAuthorization(config, TOKEN_SOURCE_TYPE_COOKIE)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWTTwoFactorAuthorization verifies whether current request is valid by 2fa passcode
|
// JWTTwoFactorAuthorization verifies whether current request is valid by 2fa passcode
|
||||||
func JWTTwoFactorAuthorization(c *core.WebContext) {
|
func JWTTwoFactorAuthorization(config *settings.Config) core.MiddlewareHandlerFunc {
|
||||||
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_HEADER)
|
return func(c *core.WebContext) {
|
||||||
|
claims, tokenContext, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_HEADER)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.PrintJsonErrorResult(c, err)
|
utils.PrintJsonErrorResult(c, err)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Type != core.USER_TOKEN_TYPE_REQUIRE_2FA {
|
||||||
|
log.Warnf(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%d\" token is not need two-factor authorization", claims.Uid)
|
||||||
|
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenNotRequire2FA)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext(tokenContext)
|
||||||
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims.Type != core.USER_TOKEN_TYPE_REQUIRE_2FA {
|
|
||||||
log.Warnf(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%d\" token is not need two-factor authorization", claims.Uid)
|
|
||||||
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenNotRequire2FA)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SetTokenClaims(claims)
|
|
||||||
c.Next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWTEmailVerifyAuthorization verifies whether current request is email verification
|
// JWTEmailVerifyAuthorization verifies whether current request is email verification
|
||||||
func JWTEmailVerifyAuthorization(c *core.WebContext) {
|
func JWTEmailVerifyAuthorization(config *settings.Config) core.MiddlewareHandlerFunc {
|
||||||
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_ARGUMENT)
|
return func(c *core.WebContext) {
|
||||||
|
claims, tokenContext, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_ARGUMENT)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.PrintJsonErrorResult(c, errs.ErrEmailVerifyTokenIsInvalidOrExpired)
|
utils.PrintJsonErrorResult(c, errs.ErrEmailVerifyTokenIsInvalidOrExpired)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Type != core.USER_TOKEN_TYPE_EMAIL_VERIFY {
|
||||||
|
log.Warnf(c, "[authorization.JWTEmailVerifyAuthorization] user \"uid:%d\" token is not for email verification", claims.Uid)
|
||||||
|
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidToken)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext(tokenContext)
|
||||||
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims.Type != core.USER_TOKEN_TYPE_EMAIL_VERIFY {
|
|
||||||
log.Warnf(c, "[authorization.JWTEmailVerifyAuthorization] user \"uid:%d\" token is not for email verification", claims.Uid)
|
|
||||||
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidToken)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SetTokenClaims(claims)
|
|
||||||
c.Next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWTResetPasswordAuthorization verifies whether current request is password reset
|
// JWTResetPasswordAuthorization verifies whether current request is password reset
|
||||||
func JWTResetPasswordAuthorization(c *core.WebContext) {
|
func JWTResetPasswordAuthorization(config *settings.Config) core.MiddlewareHandlerFunc {
|
||||||
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_ARGUMENT)
|
return func(c *core.WebContext) {
|
||||||
|
claims, tokenContext, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_ARGUMENT)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.PrintJsonErrorResult(c, errs.ErrPasswordResetTokenIsInvalidOrExpired)
|
utils.PrintJsonErrorResult(c, errs.ErrPasswordResetTokenIsInvalidOrExpired)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Type != core.USER_TOKEN_TYPE_PASSWORD_RESET {
|
||||||
|
log.Warnf(c, "[authorization.JWTResetPasswordAuthorization] user \"uid:%d\" token is not for password request", claims.Uid)
|
||||||
|
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidToken)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext(tokenContext)
|
||||||
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims.Type != core.USER_TOKEN_TYPE_PASSWORD_RESET {
|
|
||||||
log.Warnf(c, "[authorization.JWTResetPasswordAuthorization] user \"uid:%d\" token is not for password request", claims.Uid)
|
|
||||||
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidToken)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SetTokenClaims(claims)
|
|
||||||
c.Next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWTMCPAuthorization verifies whether current request is valid by jwt mcp token in header
|
// JWTMCPAuthorization verifies whether current request is valid by jwt mcp token in header
|
||||||
func JWTMCPAuthorization(c *core.WebContext) {
|
func JWTMCPAuthorization(config *settings.Config) core.MiddlewareHandlerFunc {
|
||||||
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_HEADER)
|
return func(c *core.WebContext) {
|
||||||
|
claims, tokenContext, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_HEADER)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.PrintJsonErrorResult(c, err)
|
utils.PrintJsonErrorResult(c, err)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Type != core.USER_TOKEN_TYPE_MCP {
|
||||||
|
log.Warnf(c, "[authorization.jwtAuthorization] user \"uid:%d\" token type (%d) is not mcp token", claims.Uid, claims.Type)
|
||||||
|
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext(tokenContext)
|
||||||
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims.Type != core.USER_TOKEN_TYPE_MCP {
|
|
||||||
log.Warnf(c, "[authorization.jwtAuthorization] user \"uid:%d\" token type (%d) is not mcp token", claims.Uid, claims.Type)
|
|
||||||
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SetTokenClaims(claims)
|
|
||||||
c.Next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func jwtAuthorization(c *core.WebContext, source TokenSourceType) {
|
// JWTOAuth2CallbackAuthorization verifies whether current request is OAuth 2.0 callback
|
||||||
claims, err := getTokenClaims(c, source)
|
func JWTOAuth2CallbackAuthorization(config *settings.Config) core.MiddlewareHandlerFunc {
|
||||||
|
return func(c *core.WebContext) {
|
||||||
|
claims, tokenContext, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_HEADER)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.PrintJsonErrorResult(c, err)
|
utils.PrintJsonErrorResult(c, errs.ErrTokenExpired)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Type != core.USER_TOKEN_TYPE_OAUTH2_CALLBACK && claims.Type != core.USER_TOKEN_TYPE_OAUTH2_CALLBACK_REQUIRE_VERIFY {
|
||||||
|
log.Warnf(c, "[authorization.JWTOAuth2CallbackAuthorization] user \"uid:%d\" token is not for oauth 2.0 callback request", claims.Uid)
|
||||||
|
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidToken)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext(tokenContext)
|
||||||
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims.Type == core.USER_TOKEN_TYPE_REQUIRE_2FA {
|
|
||||||
log.Warnf(c, "[authorization.jwtAuthorization] user \"uid:%d\" token requires 2fa", claims.Uid)
|
|
||||||
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenRequire2FA)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if claims.Type != core.USER_TOKEN_TYPE_NORMAL {
|
|
||||||
log.Warnf(c, "[authorization.jwtAuthorization] user \"uid:%d\" token type (%d) is invalid", claims.Uid, claims.Type)
|
|
||||||
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SetTokenClaims(claims)
|
|
||||||
c.Next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTokenClaims(c *core.WebContext, source TokenSourceType) (*core.UserTokenClaims, *errs.Error) {
|
func jwtAuthorization(config *settings.Config, source TokenSourceType) core.MiddlewareHandlerFunc {
|
||||||
token, claims, err := parseToken(c, source)
|
return func(c *core.WebContext) {
|
||||||
|
claims, tokenContext, err := getTokenClaims(c, source)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.PrintJsonErrorResult(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Type == core.USER_TOKEN_TYPE_REQUIRE_2FA {
|
||||||
|
log.Warnf(c, "[authorization.jwtAuthorization] user \"uid:%d\" token requires 2fa", claims.Uid)
|
||||||
|
utils.PrintJsonErrorResult(c, errs.ErrCurrentTokenRequire2FA)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Type != core.USER_TOKEN_TYPE_NORMAL && claims.Type != core.USER_TOKEN_TYPE_API {
|
||||||
|
log.Warnf(c, "[authorization.jwtAuthorization] user \"uid:%d\" token type (%d) is invalid", claims.Uid, claims.Type)
|
||||||
|
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Type == core.USER_TOKEN_TYPE_API && !config.EnableAPIToken {
|
||||||
|
log.Warnf(c, "[authorization.jwtAuthorization] api token is not enabled")
|
||||||
|
utils.PrintJsonErrorResult(c, errs.ErrAPITokenNotEnabled)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
c.SetTokenContext(tokenContext)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTokenClaims(c *core.WebContext, source TokenSourceType) (*core.UserTokenClaims, string, *errs.Error) {
|
||||||
|
token, claims, tokenContext, err := parseToken(c, source)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf(c, "[authorization.getTokenClaims] failed to parse token, because %s", err.Error())
|
log.Warnf(c, "[authorization.getTokenClaims] failed to parse token, because %s", err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrUnauthorizedAccess)
|
return nil, "", errs.Or(err, errs.ErrUnauthorizedAccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !token.Valid {
|
if !token.Valid {
|
||||||
log.Warnf(c, "[authorization.getTokenClaims] token is invalid")
|
log.Warnf(c, "[authorization.getTokenClaims] token is invalid")
|
||||||
return nil, errs.ErrCurrentInvalidToken
|
return nil, "", errs.ErrCurrentInvalidToken
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims.Uid <= 0 {
|
if claims.Uid <= 0 {
|
||||||
log.Warnf(c, "[authorization.getTokenClaims] user id in token is invalid")
|
log.Warnf(c, "[authorization.getTokenClaims] user id in token is invalid")
|
||||||
return nil, errs.ErrCurrentInvalidToken
|
return nil, "", errs.ErrCurrentInvalidToken
|
||||||
}
|
}
|
||||||
|
|
||||||
return claims, nil
|
return claims, tokenContext, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseToken(c *core.WebContext, source TokenSourceType) (*jwt.Token, *core.UserTokenClaims, error) {
|
func parseToken(c *core.WebContext, source TokenSourceType) (*jwt.Token, *core.UserTokenClaims, string, error) {
|
||||||
tokenString := ""
|
tokenString := ""
|
||||||
|
|
||||||
if source == TOKEN_SOURCE_TYPE_ARGUMENT {
|
if source == TOKEN_SOURCE_TYPE_ARGUMENT {
|
||||||
@@ -168,7 +212,7 @@ func parseToken(c *core.WebContext, source TokenSourceType) (*jwt.Token, *core.U
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tokenString == "" {
|
if tokenString == "" {
|
||||||
return nil, nil, errs.ErrTokenIsEmpty
|
return nil, nil, "", errs.ErrTokenIsEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
return services.Tokens.ParseToken(c, tokenString)
|
return services.Tokens.ParseToken(c, tokenString)
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// OAuth2LoginRequest represents all parameters of OAuth 2.0 login request
|
||||||
|
type OAuth2LoginRequest struct {
|
||||||
|
Platform string `form:"platform" binding:"required"`
|
||||||
|
ClientSessionId string `form:"client_session_id" binding:"required"`
|
||||||
|
Token string `form:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuth2CallbackRequest represents all parameters of OAuth 2.0 callback request
|
||||||
|
type OAuth2CallbackRequest struct {
|
||||||
|
State string `form:"state"`
|
||||||
|
Code string `form:"code"`
|
||||||
|
Error string `form:"error"`
|
||||||
|
ErrorDescription string `form:"error_description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuth2CallbackLoginRequest represents all parameters of OAuth 2.0 callback login request
|
||||||
|
type OAuth2CallbackLoginRequest struct {
|
||||||
|
Password string `json:"password" binding:"omitempty,min=6,max=128"`
|
||||||
|
Passcode string `json:"passcode" binding:"omitempty,notBlank,len=6"`
|
||||||
|
Token string `json:"token" binding:"omitempty"`
|
||||||
|
}
|
||||||
@@ -12,14 +12,29 @@ type TokenRecord struct {
|
|||||||
TokenType core.TokenType `xorm:"INDEX(IDX_token_record_uid_type_expired_time) TINYINT NOT NULL"`
|
TokenType core.TokenType `xorm:"INDEX(IDX_token_record_uid_type_expired_time) TINYINT NOT NULL"`
|
||||||
Secret string `xorm:"VARCHAR(10) NOT NULL"`
|
Secret string `xorm:"VARCHAR(10) NOT NULL"`
|
||||||
UserAgent string `xorm:"VARCHAR(255)"`
|
UserAgent string `xorm:"VARCHAR(255)"`
|
||||||
|
Context string `xorm:"BLOB"`
|
||||||
CreatedUnixTime int64 `xorm:"PK"`
|
CreatedUnixTime int64 `xorm:"PK"`
|
||||||
ExpiredUnixTime int64 `xorm:"INDEX(IDX_token_record_uid_type_expired_time) INDEX(IDX_token_record_expired_time)"`
|
ExpiredUnixTime int64 `xorm:"INDEX(IDX_token_record_uid_type_expired_time) INDEX(IDX_token_record_expired_time)"`
|
||||||
LastSeenUnixTime int64
|
LastSeenUnixTime int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OAuth2CallbackTokenContext represents the context data of oauth 2.0 callback token
|
||||||
|
type OAuth2CallbackTokenContext struct {
|
||||||
|
ExternalAuthType core.UserExternalAuthType `json:"externalAuthType"`
|
||||||
|
ExternalUsername string `json:"externalUsername"`
|
||||||
|
ExternalEmail string `json:"externalEmail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenGenerateAPIRequest represents all parameters of api token generation request
|
||||||
|
type TokenGenerateAPIRequest struct {
|
||||||
|
ExpiredInSeconds int64 `json:"expiresInSeconds" binding:"omitempty,min=0,max=4294967295"`
|
||||||
|
Password string `json:"password" binding:"omitempty,min=6,max=128"`
|
||||||
|
}
|
||||||
|
|
||||||
// TokenGenerateMCPRequest represents all parameters of mcp token generation request
|
// TokenGenerateMCPRequest represents all parameters of mcp token generation request
|
||||||
type TokenGenerateMCPRequest struct {
|
type TokenGenerateMCPRequest struct {
|
||||||
Password string `json:"password" binding:"omitempty,min=6,max=128"`
|
ExpiredInSeconds int64 `json:"expiresInSeconds" binding:"omitempty,min=0,max=4294967295"`
|
||||||
|
Password string `json:"password" binding:"omitempty,min=6,max=128"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenRevokeRequest represents all parameters of token revoking request
|
// TokenRevokeRequest represents all parameters of token revoking request
|
||||||
@@ -27,6 +42,12 @@ type TokenRevokeRequest struct {
|
|||||||
TokenId string `json:"tokenId" binding:"required,notBlank"`
|
TokenId string `json:"tokenId" binding:"required,notBlank"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TokenGenerateAPIResponse represents all response parameters of generated api token
|
||||||
|
type TokenGenerateAPIResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
APIBaseUrl string `json:"apiBaseUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
// TokenGenerateMCPResponse represents all response parameters of generated mcp token
|
// TokenGenerateMCPResponse represents all response parameters of generated mcp token
|
||||||
type TokenGenerateMCPResponse struct {
|
type TokenGenerateMCPResponse struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
|||||||
@@ -37,6 +37,15 @@ func (t TransactionType) ToTransactionDbType() (TransactionDbType, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TransactionRelatedAccountType represents related account type in transaction
|
||||||
|
type TransactionRelatedAccountType byte
|
||||||
|
|
||||||
|
// Transaction relation types
|
||||||
|
const (
|
||||||
|
TRANSACTION_RELATED_ACCOUNT_TYPE_TRANSFER_FROM TransactionRelatedAccountType = 1
|
||||||
|
TRANSACTION_RELATED_ACCOUNT_TYPE_TRANSFER_TO TransactionRelatedAccountType = 2
|
||||||
|
)
|
||||||
|
|
||||||
// TransactionDbType represents transaction type in database
|
// TransactionDbType represents transaction type in database
|
||||||
type TransactionDbType byte
|
type TransactionDbType byte
|
||||||
|
|
||||||
@@ -84,6 +93,17 @@ func (t TransactionDbType) ToTransactionType() (TransactionType, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToTransactionRelatedAccountType returns the related account type for this db enum
|
||||||
|
func (t TransactionDbType) ToTransactionRelatedAccountType() (TransactionRelatedAccountType, error) {
|
||||||
|
if t == TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||||
|
return TRANSACTION_RELATED_ACCOUNT_TYPE_TRANSFER_TO, nil
|
||||||
|
} else if t == TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||||
|
return TRANSACTION_RELATED_ACCOUNT_TYPE_TRANSFER_FROM, nil
|
||||||
|
} else {
|
||||||
|
return 0, errs.ErrTransactionTypeInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TransactionTagFilterType represents transaction tag filter type
|
// TransactionTagFilterType represents transaction tag filter type
|
||||||
type TransactionTagFilterType byte
|
type TransactionTagFilterType byte
|
||||||
|
|
||||||
@@ -255,6 +275,12 @@ type TransactionStatisticTrendsRequest struct {
|
|||||||
UseTransactionTimezone bool `form:"use_transaction_timezone"`
|
UseTransactionTimezone bool `form:"use_transaction_timezone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TransactionStatisticAssetTrendsRequest represents all parameters of transaction statistic asset trends request
|
||||||
|
type TransactionStatisticAssetTrendsRequest struct {
|
||||||
|
StartTime int64 `form:"start_time"`
|
||||||
|
EndTime int64 `form:"end_time"`
|
||||||
|
}
|
||||||
|
|
||||||
// TransactionAmountsRequest represents all parameters of transaction amounts request
|
// TransactionAmountsRequest represents all parameters of transaction amounts request
|
||||||
type TransactionAmountsRequest struct {
|
type TransactionAmountsRequest struct {
|
||||||
Query string `form:"query"`
|
Query string `form:"query"`
|
||||||
@@ -279,6 +305,12 @@ type TransactionGetRequest struct {
|
|||||||
TrimTag bool `form:"trim_tag"`
|
TrimTag bool `form:"trim_tag"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TransactionMoveBetweenAccountsRequest represents all parameters of moving all transactions between accounts request
|
||||||
|
type TransactionMoveBetweenAccountsRequest struct {
|
||||||
|
FromAccountId int64 `json:"fromAccountId,string" binding:"required,min=1"`
|
||||||
|
ToAccountId int64 `json:"toAccountId,string" binding:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
// TransactionDeleteRequest represents all parameters of transaction deleting request
|
// TransactionDeleteRequest represents all parameters of transaction deleting request
|
||||||
type TransactionDeleteRequest struct {
|
type TransactionDeleteRequest struct {
|
||||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||||
@@ -363,9 +395,11 @@ type TransactionStatisticResponse struct {
|
|||||||
|
|
||||||
// TransactionStatisticResponseItem represents total amount item for a response
|
// TransactionStatisticResponseItem represents total amount item for a response
|
||||||
type TransactionStatisticResponseItem struct {
|
type TransactionStatisticResponseItem struct {
|
||||||
CategoryId int64 `json:"categoryId,string"`
|
CategoryId int64 `json:"categoryId,string"`
|
||||||
AccountId int64 `json:"accountId,string"`
|
AccountId int64 `json:"accountId,string"`
|
||||||
TotalAmount int64 `json:"amount"`
|
RelatedAccountId int64 `json:"relatedAccountId,string,omitempty"`
|
||||||
|
RelatedAccountType TransactionRelatedAccountType `json:"relatedAccountType,omitempty"`
|
||||||
|
TotalAmount int64 `json:"amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionStatisticTrendsResponseItem represents the data within each statistic interval
|
// TransactionStatisticTrendsResponseItem represents the data within each statistic interval
|
||||||
@@ -375,6 +409,21 @@ type TransactionStatisticTrendsResponseItem struct {
|
|||||||
Items []*TransactionStatisticResponseItem `json:"items"`
|
Items []*TransactionStatisticResponseItem `json:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TransactionStatisticAssetTrendsResponseItem represents the data within each statistic interval
|
||||||
|
type TransactionStatisticAssetTrendsResponseItem struct {
|
||||||
|
Year int32 `json:"year"`
|
||||||
|
Month int32 `json:"month"`
|
||||||
|
Day int32 `json:"day"`
|
||||||
|
Items []*TransactionStatisticAssetTrendsResponseDataItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionStatisticAssetTrendsResponseDataItem represents an asset trends data item
|
||||||
|
type TransactionStatisticAssetTrendsResponseDataItem struct {
|
||||||
|
AccountId int64 `json:"accountId,string"`
|
||||||
|
AccountOpeningBalance int64 `json:"accountOpeningBalance"`
|
||||||
|
AccountClosingBalance int64 `json:"accountClosingBalance"`
|
||||||
|
}
|
||||||
|
|
||||||
// TransactionAmountsResponseItem represents an item of transaction amounts
|
// TransactionAmountsResponseItem represents an item of transaction amounts
|
||||||
type TransactionAmountsResponseItem struct {
|
type TransactionAmountsResponseItem struct {
|
||||||
StartTime int64 `json:"startTime"`
|
StartTime int64 `json:"startTime"`
|
||||||
@@ -572,6 +621,32 @@ func (s TransactionStatisticTrendsResponseItemSlice) Less(i, j int) bool {
|
|||||||
return s[i].Month < s[j].Month
|
return s[i].Month < s[j].Month
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TransactionStatisticAssetTrendsResponseItemSlice represents the slice data structure of TransactionStatisticAssetTrendsResponseItem
|
||||||
|
type TransactionStatisticAssetTrendsResponseItemSlice []*TransactionStatisticAssetTrendsResponseItem
|
||||||
|
|
||||||
|
// Len returns the count of items
|
||||||
|
func (s TransactionStatisticAssetTrendsResponseItemSlice) Len() int {
|
||||||
|
return len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap swaps two items
|
||||||
|
func (s TransactionStatisticAssetTrendsResponseItemSlice) 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 TransactionStatisticAssetTrendsResponseItemSlice) Less(i, j int) bool {
|
||||||
|
if s[i].Year != s[j].Year {
|
||||||
|
return s[i].Year < s[j].Year
|
||||||
|
}
|
||||||
|
|
||||||
|
if s[i].Month != s[j].Month {
|
||||||
|
return s[i].Month < s[j].Month
|
||||||
|
}
|
||||||
|
|
||||||
|
return s[i].Day < s[j].Day
|
||||||
|
}
|
||||||
|
|
||||||
// TransactionAmountsResponseItemAmountInfoSlice represents the slice data structure of TransactionAmountsResponseItemAmountInfo
|
// TransactionAmountsResponseItemAmountInfoSlice represents the slice data structure of TransactionAmountsResponseItemAmountInfo
|
||||||
type TransactionAmountsResponseItemAmountInfoSlice []*TransactionAmountsResponseItemAmountInfo
|
type TransactionAmountsResponseItemAmountInfoSlice []*TransactionAmountsResponseItemAmountInfo
|
||||||
|
|
||||||
|
|||||||
@@ -164,6 +164,61 @@ func TestTransactionStatisticTrendsResponseItemSliceLess(t *testing.T) {
|
|||||||
assert.Equal(t, int32(9), transactionTrendsSlice[4].Month)
|
assert.Equal(t, int32(9), transactionTrendsSlice[4].Month)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTransactionStatisticAssetTrendsResponseItemSliceLess(t *testing.T) {
|
||||||
|
var transactionTrendsSlice TransactionStatisticAssetTrendsResponseItemSlice
|
||||||
|
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{
|
||||||
|
Year: 2024,
|
||||||
|
Month: 9,
|
||||||
|
Day: 1,
|
||||||
|
})
|
||||||
|
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{
|
||||||
|
Year: 2024,
|
||||||
|
Month: 9,
|
||||||
|
Day: 2,
|
||||||
|
})
|
||||||
|
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{
|
||||||
|
Year: 2024,
|
||||||
|
Month: 10,
|
||||||
|
Day: 1,
|
||||||
|
})
|
||||||
|
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{
|
||||||
|
Year: 2022,
|
||||||
|
Month: 10,
|
||||||
|
Day: 1,
|
||||||
|
})
|
||||||
|
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{
|
||||||
|
Year: 2023,
|
||||||
|
Month: 1,
|
||||||
|
Day: 1,
|
||||||
|
})
|
||||||
|
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{
|
||||||
|
Year: 2024,
|
||||||
|
Month: 2,
|
||||||
|
Day: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
sort.Sort(transactionTrendsSlice)
|
||||||
|
|
||||||
|
assert.Equal(t, int32(2022), transactionTrendsSlice[0].Year)
|
||||||
|
assert.Equal(t, int32(10), transactionTrendsSlice[0].Month)
|
||||||
|
assert.Equal(t, int32(1), transactionTrendsSlice[0].Day)
|
||||||
|
assert.Equal(t, int32(2023), transactionTrendsSlice[1].Year)
|
||||||
|
assert.Equal(t, int32(1), transactionTrendsSlice[1].Month)
|
||||||
|
assert.Equal(t, int32(1), transactionTrendsSlice[1].Day)
|
||||||
|
assert.Equal(t, int32(2024), transactionTrendsSlice[2].Year)
|
||||||
|
assert.Equal(t, int32(2), transactionTrendsSlice[2].Month)
|
||||||
|
assert.Equal(t, int32(2), transactionTrendsSlice[2].Day)
|
||||||
|
assert.Equal(t, int32(2024), transactionTrendsSlice[3].Year)
|
||||||
|
assert.Equal(t, int32(9), transactionTrendsSlice[3].Month)
|
||||||
|
assert.Equal(t, int32(1), transactionTrendsSlice[3].Day)
|
||||||
|
assert.Equal(t, int32(2024), transactionTrendsSlice[4].Year)
|
||||||
|
assert.Equal(t, int32(9), transactionTrendsSlice[4].Month)
|
||||||
|
assert.Equal(t, int32(2), transactionTrendsSlice[4].Day)
|
||||||
|
assert.Equal(t, int32(2024), transactionTrendsSlice[5].Year)
|
||||||
|
assert.Equal(t, int32(10), transactionTrendsSlice[5].Month)
|
||||||
|
assert.Equal(t, int32(1), transactionTrendsSlice[5].Day)
|
||||||
|
}
|
||||||
|
|
||||||
func TestTransactionAmountsResponseItemAmountInfoSliceLess(t *testing.T) {
|
func TestTransactionAmountsResponseItemAmountInfoSliceLess(t *testing.T) {
|
||||||
var amountInfoSlice TransactionAmountsResponseItemAmountInfoSlice
|
var amountInfoSlice TransactionAmountsResponseItemAmountInfoSlice
|
||||||
amountInfoSlice = append(amountInfoSlice, &TransactionAmountsResponseItemAmountInfo{
|
amountInfoSlice = append(amountInfoSlice, &TransactionAmountsResponseItemAmountInfo{
|
||||||
|
|||||||
+4
-2
@@ -161,7 +161,7 @@ type UserLoginRequest struct {
|
|||||||
type UserRegisterRequest struct {
|
type UserRegisterRequest struct {
|
||||||
Username string `json:"username" binding:"required,notBlank,max=32,validUsername"`
|
Username string `json:"username" binding:"required,notBlank,max=32,validUsername"`
|
||||||
Email string `json:"email" binding:"required,notBlank,max=100,validEmail"`
|
Email string `json:"email" binding:"required,notBlank,max=100,validEmail"`
|
||||||
Nickname string `json:"nickname" binding:"required,notBlank,max=64"`
|
Nickname string `json:"nickname" binding:"required,notBlank,max=64,validNickname"`
|
||||||
Password string `json:"password" binding:"required,min=6,max=128"`
|
Password string `json:"password" binding:"required,min=6,max=128"`
|
||||||
Language string `json:"language" binding:"required,min=2,max=16"`
|
Language string `json:"language" binding:"required,min=2,max=16"`
|
||||||
DefaultCurrency string `json:"defaultCurrency" binding:"required,len=3,validCurrency"`
|
DefaultCurrency string `json:"defaultCurrency" binding:"required,len=3,validCurrency"`
|
||||||
@@ -190,7 +190,7 @@ type UserResendVerifyEmailRequest struct {
|
|||||||
// UserProfileUpdateRequest represents all parameters of user updating profile request
|
// UserProfileUpdateRequest represents all parameters of user updating profile request
|
||||||
type UserProfileUpdateRequest struct {
|
type UserProfileUpdateRequest struct {
|
||||||
Email string `json:"email" binding:"omitempty,notBlank,max=100,validEmail"`
|
Email string `json:"email" binding:"omitempty,notBlank,max=100,validEmail"`
|
||||||
Nickname string `json:"nickname" binding:"omitempty,notBlank,max=64"`
|
Nickname string `json:"nickname" binding:"omitempty,notBlank,max=64,validNickname"`
|
||||||
Password string `json:"password" binding:"omitempty,min=6,max=128"`
|
Password string `json:"password" binding:"omitempty,min=6,max=128"`
|
||||||
OldPassword string `json:"oldPassword" 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"`
|
DefaultAccountId int64 `json:"defaultAccountId,string" binding:"omitempty,min=1"`
|
||||||
@@ -225,6 +225,7 @@ type UserProfileUpdateResponse struct {
|
|||||||
// UserProfileResponse represents a view-object of user profile
|
// UserProfileResponse represents a view-object of user profile
|
||||||
type UserProfileResponse struct {
|
type UserProfileResponse struct {
|
||||||
*UserBasicInfo
|
*UserBasicInfo
|
||||||
|
NoPassword bool `json:"noPassword,omitempty"`
|
||||||
LastLoginAt int64 `json:"lastLoginAt"`
|
LastLoginAt int64 `json:"lastLoginAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +314,7 @@ func (u *User) ToUserBasicInfo(avatarProvider core.UserAvatarProviderType, avata
|
|||||||
func (u *User) ToUserProfileResponse(basicInfo *UserBasicInfo) *UserProfileResponse {
|
func (u *User) ToUserProfileResponse(basicInfo *UserBasicInfo) *UserProfileResponse {
|
||||||
return &UserProfileResponse{
|
return &UserProfileResponse{
|
||||||
UserBasicInfo: basicInfo,
|
UserBasicInfo: basicInfo,
|
||||||
|
NoPassword: u.Password == "",
|
||||||
LastLoginAt: u.LastLoginUnixTime,
|
LastLoginAt: u.LastLoginUnixTime,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ var ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES = map[string]UserApplicationClo
|
|||||||
"statistics.defaultCategoricalChartDataRangeType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
|
"statistics.defaultCategoricalChartDataRangeType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
|
||||||
"statistics.defaultTrendChartType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
|
"statistics.defaultTrendChartType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
|
||||||
"statistics.defaultTrendChartDataRangeType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
|
"statistics.defaultTrendChartDataRangeType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
|
||||||
|
"statistics.defaultAssetTrendsChartType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
|
||||||
|
"statistics.defaultAssetTrendsChartDataRangeType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserApplicationCloudSetting represents user application cloud setting stored in database
|
// UserApplicationCloudSetting represents user application cloud setting stored in database
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
|
||||||
|
// UserExternalAuth represents user external auth data stored in database
|
||||||
|
type UserExternalAuth struct {
|
||||||
|
Uid int64 `xorm:"PK"`
|
||||||
|
ExternalAuthType core.UserExternalAuthType `xorm:"VARCHAR(32) PK UNIQUE(uqe_userexternalauth_authtype_username) UNIQUE(uqe_userexternalauth_authtype_email)"`
|
||||||
|
ExternalUsername string `xorm:"VARCHAR(32) UNIQUE(uqe_userexternalauth_authtype_username) NOT NULL"`
|
||||||
|
ExternalEmail string `xorm:"VARCHAR(100) UNIQUE(uqe_userexternalauth_authtype_email) NOT NULL"`
|
||||||
|
CreatedUnixTime int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserExternalAuthUnlinkRequest represents all parameters of user external auth unlink request
|
||||||
|
type UserExternalAuthUnlinkRequest struct {
|
||||||
|
ExternalAuthType string `json:"externalAuthType" binding:"required,notBlank"`
|
||||||
|
Password string `json:"password" binding:"required,min=6,max=128"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserExternalAuthInfoResponse represents a view-object of user external auth
|
||||||
|
type UserExternalAuthInfoResponse struct {
|
||||||
|
ExternalAuthCategory string `json:"externalAuthCategory"`
|
||||||
|
ExternalAuthType core.UserExternalAuthType `json:"externalAuthType"`
|
||||||
|
Linked bool `json:"linked"`
|
||||||
|
ExternalUsername string `json:"externalUsername,omitempty"`
|
||||||
|
CreatedAt int64 `json:"createdAt,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToUserExternalAuthInfoResponse returns a view-object according to database model
|
||||||
|
func (a *UserExternalAuth) ToUserExternalAuthInfoResponse() *UserExternalAuthInfoResponse {
|
||||||
|
return &UserExternalAuthInfoResponse{
|
||||||
|
ExternalAuthCategory: a.ExternalAuthType.GetCategory(),
|
||||||
|
ExternalAuthType: a.ExternalAuthType,
|
||||||
|
Linked: true,
|
||||||
|
ExternalUsername: a.ExternalUsername,
|
||||||
|
CreatedAt: a.CreatedUnixTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserExternalAuthInfoResponsesSlice represents the slice data structure of UserExternalAuthInfoResponse
|
||||||
|
type UserExternalAuthInfoResponsesSlice []*UserExternalAuthInfoResponse
|
||||||
|
|
||||||
|
// Len returns the count of items
|
||||||
|
func (a UserExternalAuthInfoResponsesSlice) Len() int {
|
||||||
|
return len(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap swaps two items
|
||||||
|
func (a UserExternalAuthInfoResponsesSlice) Swap(i, j int) {
|
||||||
|
a[i], a[j] = a[j], a[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less reports whether the first item is less than the second one
|
||||||
|
func (a UserExternalAuthInfoResponsesSlice) Less(i, j int) bool {
|
||||||
|
if a[i].Linked && !a[j].Linked {
|
||||||
|
return true
|
||||||
|
} else if !a[i].Linked && a[j].Linked {
|
||||||
|
return false
|
||||||
|
} else if !a[i].Linked && !a[j].Linked {
|
||||||
|
return a[i].ExternalAuthType < a[j].ExternalAuthType
|
||||||
|
}
|
||||||
|
|
||||||
|
return a[i].CreatedAt > a[j].CreatedAt
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserExternalAuthInfoResponsesSliceLess(t *testing.T) {
|
||||||
|
var userExternalAuthInfoResponsesSlice UserExternalAuthInfoResponsesSlice
|
||||||
|
userExternalAuthInfoResponsesSlice = append(userExternalAuthInfoResponsesSlice, &UserExternalAuthInfoResponse{
|
||||||
|
ExternalAuthType: core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_OIDC,
|
||||||
|
Linked: true,
|
||||||
|
ExternalUsername: "test",
|
||||||
|
CreatedAt: int64(1),
|
||||||
|
})
|
||||||
|
userExternalAuthInfoResponsesSlice = append(userExternalAuthInfoResponsesSlice, &UserExternalAuthInfoResponse{
|
||||||
|
ExternalAuthType: core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_NEXTCLOUD,
|
||||||
|
Linked: false,
|
||||||
|
})
|
||||||
|
userExternalAuthInfoResponsesSlice = append(userExternalAuthInfoResponsesSlice, &UserExternalAuthInfoResponse{
|
||||||
|
ExternalAuthType: core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITEA,
|
||||||
|
Linked: false,
|
||||||
|
})
|
||||||
|
userExternalAuthInfoResponsesSlice = append(userExternalAuthInfoResponsesSlice, &UserExternalAuthInfoResponse{
|
||||||
|
ExternalAuthType: core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITHUB,
|
||||||
|
Linked: true,
|
||||||
|
ExternalUsername: "test4",
|
||||||
|
CreatedAt: int64(2),
|
||||||
|
})
|
||||||
|
|
||||||
|
sort.Sort(userExternalAuthInfoResponsesSlice)
|
||||||
|
|
||||||
|
assert.Equal(t, core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITHUB, userExternalAuthInfoResponsesSlice[0].ExternalAuthType)
|
||||||
|
assert.Equal(t, core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_OIDC, userExternalAuthInfoResponsesSlice[1].ExternalAuthType)
|
||||||
|
assert.Equal(t, core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITEA, userExternalAuthInfoResponsesSlice[2].ExternalAuthType)
|
||||||
|
assert.Equal(t, core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_NEXTCLOUD, userExternalAuthInfoResponsesSlice[3].ExternalAuthType)
|
||||||
|
}
|
||||||
+82
-28
@@ -23,6 +23,9 @@ import (
|
|||||||
// TokenUserAgentCreatedViaCli is the user agent of token created via cli
|
// TokenUserAgentCreatedViaCli is the user agent of token created via cli
|
||||||
const TokenUserAgentCreatedViaCli = "ezbookkeeping Cli"
|
const TokenUserAgentCreatedViaCli = "ezbookkeeping Cli"
|
||||||
|
|
||||||
|
// TokenUserAgentForAPI is the user agent for API token
|
||||||
|
const TokenUserAgentForAPI = "ezbookkeeping API"
|
||||||
|
|
||||||
// TokenUserAgentForMCP is the user agent for MCP token
|
// TokenUserAgentForMCP is the user agent for MCP token
|
||||||
const TokenUserAgentForMCP = "ezbookkeeping MCP"
|
const TokenUserAgentForMCP = "ezbookkeeping MCP"
|
||||||
|
|
||||||
@@ -67,72 +70,120 @@ func (s *TokenService) GetAllUnexpiredNormalAndMCPTokensByUid(c core.Context, ui
|
|||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
|
|
||||||
var tokenRecords []*models.TokenRecord
|
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=? OR token_type=?) AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, core.USER_TOKEN_TYPE_MCP, now).Find(&tokenRecords)
|
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=? OR token_type=? OR token_type=?) AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, core.USER_TOKEN_TYPE_MCP, core.USER_TOKEN_TYPE_API, now).Find(&tokenRecords)
|
||||||
|
|
||||||
return tokenRecords, err
|
return tokenRecords, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseToken returns the token model according to token content
|
// ParseToken returns the token model according to token content
|
||||||
func (s *TokenService) ParseToken(c core.Context, token string) (*jwt.Token, *core.UserTokenClaims, error) {
|
func (s *TokenService) ParseToken(c core.Context, token string) (*jwt.Token, *core.UserTokenClaims, string, error) {
|
||||||
return s.parseToken(c, token)
|
return s.parseToken(c, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTokenViaCli generates a new normal token and saves to database
|
|
||||||
func (s *TokenService) CreateTokenViaCli(c *core.CliContext, user *models.User) (string, *models.TokenRecord, error) {
|
|
||||||
token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_NORMAL, TokenUserAgentCreatedViaCli, s.CurrentConfig().TokenExpiredTimeDuration)
|
|
||||||
return token, tokenRecord, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateToken generates a new normal token and saves to database
|
// CreateToken generates a new normal token and saves to database
|
||||||
func (s *TokenService) CreateToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) {
|
func (s *TokenService) CreateToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) {
|
||||||
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_NORMAL, s.getUserAgent(c), s.CurrentConfig().TokenExpiredTimeDuration)
|
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_NORMAL, s.getUserAgent(c), "", s.CurrentConfig().TokenExpiredTimeDuration)
|
||||||
return token, claims, err
|
return token, claims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRequire2FAToken generates a new token requiring user to verify 2fa passcode and saves to database
|
// CreateRequire2FAToken generates a new token requiring user to verify 2fa passcode and saves to database
|
||||||
func (s *TokenService) CreateRequire2FAToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) {
|
func (s *TokenService) CreateRequire2FAToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) {
|
||||||
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_REQUIRE_2FA, s.getUserAgent(c), s.CurrentConfig().TemporaryTokenExpiredTimeDuration)
|
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_REQUIRE_2FA, s.getUserAgent(c), "", s.CurrentConfig().TemporaryTokenExpiredTimeDuration)
|
||||||
return token, claims, err
|
return token, claims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateEmailVerifyToken generates a new email verify token and saves to database
|
// CreateEmailVerifyToken generates a new email verify token and saves to database
|
||||||
func (s *TokenService) CreateEmailVerifyToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) {
|
func (s *TokenService) CreateEmailVerifyToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) {
|
||||||
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_EMAIL_VERIFY, s.getUserAgent(c), s.CurrentConfig().EmailVerifyTokenExpiredTimeDuration)
|
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_EMAIL_VERIFY, s.getUserAgent(c), "", s.CurrentConfig().EmailVerifyTokenExpiredTimeDuration)
|
||||||
return token, claims, err
|
return token, claims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateEmailVerifyTokenWithoutUserAgent generates a new email verify token and saves to database
|
// CreateEmailVerifyTokenWithoutUserAgent generates a new email verify token and saves to database
|
||||||
func (s *TokenService) CreateEmailVerifyTokenWithoutUserAgent(c core.Context, user *models.User) (string, *core.UserTokenClaims, error) {
|
func (s *TokenService) CreateEmailVerifyTokenWithoutUserAgent(c core.Context, user *models.User) (string, *core.UserTokenClaims, error) {
|
||||||
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_EMAIL_VERIFY, "", s.CurrentConfig().EmailVerifyTokenExpiredTimeDuration)
|
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_EMAIL_VERIFY, "", "", s.CurrentConfig().EmailVerifyTokenExpiredTimeDuration)
|
||||||
return token, claims, err
|
return token, claims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatePasswordResetToken generates a new password reset token and saves to database
|
// CreatePasswordResetToken generates a new password reset token and saves to database
|
||||||
func (s *TokenService) CreatePasswordResetToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) {
|
func (s *TokenService) CreatePasswordResetToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) {
|
||||||
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_PASSWORD_RESET, s.getUserAgent(c), s.CurrentConfig().PasswordResetTokenExpiredTimeDuration)
|
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_PASSWORD_RESET, s.getUserAgent(c), "", s.CurrentConfig().PasswordResetTokenExpiredTimeDuration)
|
||||||
return token, claims, err
|
return token, claims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatePasswordResetTokenWithoutUserAgent generates a new password reset token and saves to database
|
// CreatePasswordResetTokenWithoutUserAgent generates a new password reset token and saves to database
|
||||||
func (s *TokenService) CreatePasswordResetTokenWithoutUserAgent(c core.Context, user *models.User) (string, *core.UserTokenClaims, error) {
|
func (s *TokenService) CreatePasswordResetTokenWithoutUserAgent(c core.Context, user *models.User) (string, *core.UserTokenClaims, error) {
|
||||||
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_PASSWORD_RESET, "", s.CurrentConfig().PasswordResetTokenExpiredTimeDuration)
|
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_PASSWORD_RESET, "", "", s.CurrentConfig().PasswordResetTokenExpiredTimeDuration)
|
||||||
return token, claims, err
|
return token, claims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateAPIToken generates a new API token and saves to database
|
||||||
|
func (s *TokenService) CreateAPIToken(c *core.WebContext, user *models.User, expiresInSeconds int64) (string, *core.UserTokenClaims, error) {
|
||||||
|
var tokenExpiredTimeDuration time.Duration
|
||||||
|
|
||||||
|
if expiresInSeconds > 0 {
|
||||||
|
tokenExpiredTimeDuration = time.Duration(expiresInSeconds) * time.Second
|
||||||
|
} else {
|
||||||
|
tokenExpiredTimeDuration = time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_API, s.getUserAgent(c), "", tokenExpiredTimeDuration)
|
||||||
|
return token, claims, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAPITokenViaCli generates a new API token and saves to database
|
||||||
|
func (s *TokenService) CreateAPITokenViaCli(c *core.CliContext, user *models.User, expiresInSeconds int64) (string, *models.TokenRecord, error) {
|
||||||
|
var tokenExpiredTimeDuration time.Duration
|
||||||
|
|
||||||
|
if expiresInSeconds > 0 {
|
||||||
|
tokenExpiredTimeDuration = time.Duration(expiresInSeconds) * time.Second
|
||||||
|
} else {
|
||||||
|
tokenExpiredTimeDuration = time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_API, TokenUserAgentCreatedViaCli, "", tokenExpiredTimeDuration)
|
||||||
|
return token, tokenRecord, err
|
||||||
|
}
|
||||||
|
|
||||||
// CreateMCPToken generates a new MCP token and saves to database
|
// CreateMCPToken generates a new MCP token and saves to database
|
||||||
func (s *TokenService) CreateMCPToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) {
|
func (s *TokenService) CreateMCPToken(c *core.WebContext, user *models.User, expiresInSeconds int64) (string, *core.UserTokenClaims, error) {
|
||||||
tokenExpiredTimeDuration := time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now())
|
var tokenExpiredTimeDuration time.Duration
|
||||||
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_MCP, s.getUserAgent(c), tokenExpiredTimeDuration)
|
|
||||||
|
if expiresInSeconds > 0 {
|
||||||
|
tokenExpiredTimeDuration = time.Duration(expiresInSeconds) * time.Second
|
||||||
|
} else {
|
||||||
|
tokenExpiredTimeDuration = time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_MCP, s.getUserAgent(c), "", tokenExpiredTimeDuration)
|
||||||
return token, claims, err
|
return token, claims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateMCPTokenViaCli generates a new MCP token and saves to database
|
// CreateMCPTokenViaCli generates a new MCP token and saves to database
|
||||||
func (s *TokenService) CreateMCPTokenViaCli(c *core.CliContext, user *models.User) (string, *models.TokenRecord, error) {
|
func (s *TokenService) CreateMCPTokenViaCli(c *core.CliContext, user *models.User, expiresInSeconds int64) (string, *models.TokenRecord, error) {
|
||||||
tokenExpiredTimeDuration := time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now())
|
var tokenExpiredTimeDuration time.Duration
|
||||||
token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_MCP, TokenUserAgentCreatedViaCli, tokenExpiredTimeDuration)
|
|
||||||
|
if expiresInSeconds > 0 {
|
||||||
|
tokenExpiredTimeDuration = time.Duration(expiresInSeconds) * time.Second
|
||||||
|
} else {
|
||||||
|
tokenExpiredTimeDuration = time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_MCP, TokenUserAgentCreatedViaCli, "", tokenExpiredTimeDuration)
|
||||||
return token, tokenRecord, err
|
return token, tokenRecord, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateOAuth2CallbackRequireVerifyToken generates a new OAuth 2.0 callback token requiring user to verify and saves to database
|
||||||
|
func (s *TokenService) CreateOAuth2CallbackRequireVerifyToken(c *core.WebContext, user *models.User, context string) (string, *core.UserTokenClaims, error) {
|
||||||
|
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_OAUTH2_CALLBACK_REQUIRE_VERIFY, s.getUserAgent(c), context, s.CurrentConfig().TemporaryTokenExpiredTimeDuration)
|
||||||
|
return token, claims, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOAuth2CallbackToken generates a new OAuth 2.0 callback token and saves to database
|
||||||
|
func (s *TokenService) CreateOAuth2CallbackToken(c *core.WebContext, user *models.User, context string) (string, *core.UserTokenClaims, error) {
|
||||||
|
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_OAUTH2_CALLBACK, s.getUserAgent(c), context, s.CurrentConfig().TemporaryTokenExpiredTimeDuration)
|
||||||
|
return token, claims, err
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateTokenLastSeen updates the last seen time of specified token
|
// UpdateTokenLastSeen updates the last seen time of specified token
|
||||||
func (s *TokenService) UpdateTokenLastSeen(c core.Context, tokenRecord *models.TokenRecord) error {
|
func (s *TokenService) UpdateTokenLastSeen(c core.Context, tokenRecord *models.TokenRecord) error {
|
||||||
if tokenRecord.Uid <= 0 {
|
if tokenRecord.Uid <= 0 {
|
||||||
@@ -319,8 +370,9 @@ func (s *TokenService) GenerateTokenId(tokenRecord *models.TokenRecord) string {
|
|||||||
return fmt.Sprintf("%d:%d:%d", tokenRecord.Uid, tokenRecord.CreatedUnixTime, tokenRecord.UserTokenId)
|
return fmt.Sprintf("%d:%d:%d", tokenRecord.Uid, tokenRecord.CreatedUnixTime, tokenRecord.UserTokenId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TokenService) parseToken(c core.Context, tokenString string) (*jwt.Token, *core.UserTokenClaims, error) {
|
func (s *TokenService) parseToken(c core.Context, tokenString string) (*jwt.Token, *core.UserTokenClaims, string, error) {
|
||||||
claims := &core.UserTokenClaims{}
|
claims := &core.UserTokenClaims{}
|
||||||
|
tokenContext := ""
|
||||||
|
|
||||||
token, err := jwt.ParseWithClaims(tokenString, claims,
|
token, err := jwt.ParseWithClaims(tokenString, claims,
|
||||||
func(token *jwt.Token) (any, error) {
|
func(token *jwt.Token) (any, error) {
|
||||||
@@ -344,6 +396,7 @@ func (s *TokenService) parseToken(c core.Context, tokenString string) (*jwt.Toke
|
|||||||
return nil, errs.ErrTokenExpired
|
return nil, errs.ErrTokenExpired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tokenContext = tokenRecord.Context
|
||||||
return []byte(tokenRecord.Secret), nil
|
return []byte(tokenRecord.Secret), nil
|
||||||
},
|
},
|
||||||
jwt.WithIssuedAt(),
|
jwt.WithIssuedAt(),
|
||||||
@@ -351,30 +404,30 @@ func (s *TokenService) parseToken(c core.Context, tokenString string) (*jwt.Toke
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, request.ErrNoTokenInRequest) {
|
if errors.Is(err, request.ErrNoTokenInRequest) {
|
||||||
return nil, nil, errs.ErrTokenIsEmpty
|
return nil, nil, "", errs.ErrTokenIsEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, jwt.ErrTokenMalformed) || errors.Is(err, jwt.ErrTokenUnverifiable) || errors.Is(err, jwt.ErrTokenSignatureInvalid) {
|
if errors.Is(err, jwt.ErrTokenMalformed) || errors.Is(err, jwt.ErrTokenUnverifiable) || errors.Is(err, jwt.ErrTokenSignatureInvalid) {
|
||||||
log.Warnf(c, "[tokens.parseToken] token is invalid, because %s", err.Error())
|
log.Warnf(c, "[tokens.parseToken] token is invalid, because %s", err.Error())
|
||||||
return nil, nil, errs.ErrCurrentInvalidToken
|
return nil, nil, "", errs.ErrCurrentInvalidToken
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||||
return nil, nil, errs.ErrCurrentTokenExpired
|
return nil, nil, "", errs.ErrCurrentTokenExpired
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, jwt.ErrTokenUsedBeforeIssued) {
|
if errors.Is(err, jwt.ErrTokenUsedBeforeIssued) {
|
||||||
log.Warnf(c, "[tokens.parseToken] token is invalid, because issue time is later than now")
|
log.Warnf(c, "[tokens.parseToken] token is invalid, because issue time is later than now")
|
||||||
return nil, nil, errs.ErrCurrentInvalidToken
|
return nil, nil, "", errs.ErrCurrentInvalidToken
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil, err
|
return nil, nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return token, claims, err
|
return token, claims, tokenContext, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TokenService) createToken(c core.Context, user *models.User, tokenType core.TokenType, userAgent string, expiryDate time.Duration) (string, *core.UserTokenClaims, *models.TokenRecord, error) {
|
func (s *TokenService) createToken(c core.Context, user *models.User, tokenType core.TokenType, userAgent string, context string, expiryDate time.Duration) (string, *core.UserTokenClaims, *models.TokenRecord, error) {
|
||||||
var err error
|
var err error
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
@@ -383,6 +436,7 @@ func (s *TokenService) createToken(c core.Context, user *models.User, tokenType
|
|||||||
UserTokenId: s.getUserTokenId(),
|
UserTokenId: s.getUserTokenId(),
|
||||||
TokenType: tokenType,
|
TokenType: tokenType,
|
||||||
UserAgent: userAgent,
|
UserAgent: userAgent,
|
||||||
|
Context: context,
|
||||||
CreatedUnixTime: now.Unix(),
|
CreatedUnixTime: now.Unix(),
|
||||||
ExpiredUnixTime: now.Add(expiryDate).Unix(),
|
ExpiredUnixTime: now.Add(expiryDate).Unix(),
|
||||||
LastSeenUnixTime: now.Unix(),
|
LastSeenUnixTime: now.Unix(),
|
||||||
|
|||||||
+377
-18
@@ -107,8 +107,8 @@ func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int
|
|||||||
return allTransactions, nil
|
return allTransactions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllTransactionsWithAccountBalanceByMaxTime returns account statement within time range
|
// GetAllTransactionsInOneAccountWithAccountBalanceByMaxTime returns account statement within time range
|
||||||
func (s *TransactionService) GetAllTransactionsWithAccountBalanceByMaxTime(c core.Context, uid int64, pageCount int32, maxTransactionTime int64, minTransactionTime int64, accountId int64, accountCategory models.AccountCategory) ([]*models.TransactionWithAccountBalance, int64, int64, int64, int64, error) {
|
func (s *TransactionService) GetAllTransactionsInOneAccountWithAccountBalanceByMaxTime(c core.Context, uid int64, pageCount int32, maxTransactionTime int64, minTransactionTime int64, accountId int64, accountCategory models.AccountCategory) ([]*models.TransactionWithAccountBalance, int64, int64, int64, int64, error) {
|
||||||
if maxTransactionTime <= 0 {
|
if maxTransactionTime <= 0 {
|
||||||
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix())
|
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix())
|
||||||
}
|
}
|
||||||
@@ -158,7 +158,7 @@ func (s *TransactionService) GetAllTransactionsWithAccountBalanceByMaxTime(c cor
|
|||||||
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||||
accumulatedBalance = accumulatedBalance + transaction.Amount
|
accumulatedBalance = accumulatedBalance + transaction.Amount
|
||||||
} else {
|
} else {
|
||||||
log.Errorf(c, "[transactions.GetAllTransactionsWithAccountBalanceByMaxTime] trasaction type (%d) is invalid (id:%d)", transaction.TransactionId, transaction.Type)
|
log.Errorf(c, "[transactions.GetAllTransactionsInOneAccountWithAccountBalanceByMaxTime] trasaction type (%d) is invalid (id:%d)", transaction.TransactionId, transaction.Type)
|
||||||
return nil, 0, 0, 0, 0, errs.ErrTransactionTypeInvalid
|
return nil, 0, 0, 0, 0, errs.ErrTransactionTypeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +197,132 @@ func (s *TransactionService) GetAllTransactionsWithAccountBalanceByMaxTime(c cor
|
|||||||
return allTransactionsAndAccountBalance, totalInflows, totalOutflows, openingBalance, accumulatedBalance, nil
|
return allTransactionsAndAccountBalance, totalInflows, totalOutflows, openingBalance, accumulatedBalance, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllAccountsDailyOpeningAndClosingBalance returns daily opening and closing balance of all accounts within time range
|
||||||
|
func (s *TransactionService) GetAllAccountsDailyOpeningAndClosingBalance(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, utcOffset int16) (map[int32][]*models.TransactionWithAccountBalance, error) {
|
||||||
|
if maxTransactionTime <= 0 {
|
||||||
|
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
clientLocation := time.FixedZone("Client Timezone", int(utcOffset)*60)
|
||||||
|
var allTransactions []*models.Transaction
|
||||||
|
|
||||||
|
for maxTransactionTime > 0 {
|
||||||
|
transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", "", 1, pageCountForLoadTransactionAmounts, false, false)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
allTransactions = append(allTransactions, transactions...)
|
||||||
|
|
||||||
|
if len(transactions) < pageCountForLoadTransactionAmounts {
|
||||||
|
maxTransactionTime = 0
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
accountDailyLastBalances := make(map[string]*models.TransactionWithAccountBalance)
|
||||||
|
accountDailyBalances := make(map[int32][]*models.TransactionWithAccountBalance)
|
||||||
|
|
||||||
|
if len(allTransactions) < 1 {
|
||||||
|
return accountDailyBalances, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulatedBalances := make(map[int64]int64)
|
||||||
|
accumulatedBalancesBeforeStartTime := make(map[int64]int64)
|
||||||
|
|
||||||
|
for i := len(allTransactions) - 1; i >= 0; i-- {
|
||||||
|
transaction := allTransactions[i]
|
||||||
|
accumulatedBalance := accumulatedBalances[transaction.AccountId]
|
||||||
|
lastAccumulatedBalance := accumulatedBalances[transaction.AccountId]
|
||||||
|
|
||||||
|
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||||
|
accumulatedBalance = accumulatedBalance + transaction.RelatedAccountAmount
|
||||||
|
} else if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME {
|
||||||
|
accumulatedBalance = accumulatedBalance + transaction.Amount
|
||||||
|
} else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
|
||||||
|
accumulatedBalance = accumulatedBalance - transaction.Amount
|
||||||
|
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||||
|
accumulatedBalance = accumulatedBalance - transaction.Amount
|
||||||
|
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||||
|
accumulatedBalance = accumulatedBalance + transaction.Amount
|
||||||
|
} else {
|
||||||
|
log.Errorf(c, "[transactions.GetAllTransactionsWithAccountBalanceByMaxTime] trasaction type (%d) is invalid (id:%d)", transaction.TransactionId, transaction.Type)
|
||||||
|
return nil, errs.ErrTransactionTypeInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulatedBalances[transaction.AccountId] = accumulatedBalance
|
||||||
|
|
||||||
|
if transaction.TransactionTime < minTransactionTime {
|
||||||
|
accumulatedBalancesBeforeStartTime[transaction.AccountId] = accumulatedBalance
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
yearMonthDay := utils.FormatUnixTimeToNumericYearMonthDay(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), clientLocation)
|
||||||
|
groupKey := fmt.Sprintf("%d_%d", yearMonthDay, transaction.AccountId)
|
||||||
|
dailyAccountBalance, exists := accountDailyLastBalances[groupKey]
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
dailyAccountBalance.AccountClosingBalance = accumulatedBalance
|
||||||
|
} else {
|
||||||
|
dailyAccountBalance = &models.TransactionWithAccountBalance{
|
||||||
|
Transaction: &models.Transaction{
|
||||||
|
AccountId: transaction.AccountId,
|
||||||
|
},
|
||||||
|
AccountOpeningBalance: lastAccumulatedBalance,
|
||||||
|
AccountClosingBalance: accumulatedBalance,
|
||||||
|
}
|
||||||
|
accountDailyLastBalances[groupKey] = dailyAccountBalance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
firstTransactionTime := allTransactions[len(allTransactions)-1].TransactionTime
|
||||||
|
|
||||||
|
if minTransactionTime > firstTransactionTime {
|
||||||
|
firstTransactionTime = minTransactionTime
|
||||||
|
}
|
||||||
|
|
||||||
|
firstYearMonthDay := utils.FormatUnixTimeToNumericYearMonthDay(utils.GetUnixTimeFromTransactionTime(firstTransactionTime), clientLocation)
|
||||||
|
|
||||||
|
// fill in the opening balance for accounts that do not have transactions on the first day
|
||||||
|
for accountId, accumulatedBalance := range accumulatedBalancesBeforeStartTime {
|
||||||
|
if accumulatedBalance == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
groupKey := fmt.Sprintf("%d_%d", firstYearMonthDay, accountId)
|
||||||
|
|
||||||
|
if _, exists := accountDailyLastBalances[groupKey]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
accountDailyLastBalances[groupKey] = &models.TransactionWithAccountBalance{
|
||||||
|
Transaction: &models.Transaction{
|
||||||
|
AccountId: accountId,
|
||||||
|
},
|
||||||
|
AccountOpeningBalance: accumulatedBalance,
|
||||||
|
AccountClosingBalance: accumulatedBalance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for groupKey, transactionWithAccountBalance := range accountDailyLastBalances {
|
||||||
|
groupKeyParts := strings.Split(groupKey, "_")
|
||||||
|
yearMonthDay, _ := utils.StringToInt32(groupKeyParts[0])
|
||||||
|
dailyAccountBalances, exists := accountDailyBalances[yearMonthDay]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
dailyAccountBalances = make([]*models.TransactionWithAccountBalance, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
dailyAccountBalances = append(dailyAccountBalances, transactionWithAccountBalance)
|
||||||
|
accountDailyBalances[yearMonthDay] = dailyAccountBalances
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountDailyBalances, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetTransactionsByMaxTime returns transactions before given time
|
// GetTransactionsByMaxTime returns transactions before given time
|
||||||
func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, 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.TransactionType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, amountFilter string, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) {
|
||||||
if uid <= 0 {
|
if uid <= 0 {
|
||||||
@@ -1168,6 +1294,219 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TransactionService) MoveAllTransactionsBetweenAccounts(c core.Context, uid int64, fromAccountId int64, toAccountId int64) error {
|
||||||
|
if uid <= 0 {
|
||||||
|
return errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
if fromAccountId <= 0 || toAccountId <= 0 {
|
||||||
|
return errs.ErrAccountIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
if fromAccountId == toAccountId {
|
||||||
|
return errs.ErrCannotMoveTransactionToSameAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||||
|
// get and verify from and to account
|
||||||
|
fromAccount := &models.Account{}
|
||||||
|
has, err := sess.ID(fromAccountId).Where("uid=? AND deleted=?", uid, false).Get(fromAccount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !has {
|
||||||
|
return errs.ErrAccountNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
toAccount := &models.Account{}
|
||||||
|
has, err = sess.ID(toAccountId).Where("uid=? AND deleted=?", uid, false).Get(toAccount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !has {
|
||||||
|
return errs.ErrAccountNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if fromAccount.Hidden || toAccount.Hidden {
|
||||||
|
return errs.ErrCannotMoveTransactionFromOrToHiddenAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
if fromAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS || toAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS {
|
||||||
|
return errs.ErrCannotMoveTransactionFromOrToParentAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
if fromAccount.Currency != toAccount.Currency {
|
||||||
|
return errs.ErrCannotMoveTransactionBetweenAccountsWithDifferentCurrencies
|
||||||
|
}
|
||||||
|
|
||||||
|
// combine balance modification transaction
|
||||||
|
var balanceModificationTransactions []*models.Transaction
|
||||||
|
err = sess.Where("uid=? AND deleted=? AND type=? AND (account_id=? OR account_id=?)", uid, false, models.TRANSACTION_DB_TYPE_MODIFY_BALANCE, fromAccountId, toAccountId).Find(&balanceModificationTransactions)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(balanceModificationTransactions) > 2 {
|
||||||
|
log.Errorf(c, "[transactions.MoveAllTransactionsBetweenAccounts] user \"uid:%d\" has more than 2 balance modification transactions in account \"id:%d\" and account \"id:%d\", cannot combine balance modification transaction", uid, fromAccountId, toAccountId)
|
||||||
|
return errs.ErrOperationFailed
|
||||||
|
} else if len(balanceModificationTransactions) == 2 && balanceModificationTransactions[0].AccountId != balanceModificationTransactions[1].AccountId {
|
||||||
|
// if two balance modification transactions exist, merge the amounts into the earlier one and delete the later transaction
|
||||||
|
var earlierTransaction *models.Transaction
|
||||||
|
var laterTransaction *models.Transaction
|
||||||
|
|
||||||
|
if balanceModificationTransactions[0].TransactionTime < balanceModificationTransactions[1].TransactionTime {
|
||||||
|
earlierTransaction = balanceModificationTransactions[0]
|
||||||
|
laterTransaction = balanceModificationTransactions[1]
|
||||||
|
} else {
|
||||||
|
earlierTransaction = balanceModificationTransactions[1]
|
||||||
|
laterTransaction = balanceModificationTransactions[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
earlierTransaction.Amount += laterTransaction.Amount
|
||||||
|
earlierTransaction.RelatedAccountAmount += laterTransaction.RelatedAccountAmount
|
||||||
|
earlierTransaction.UpdatedUnixTime = time.Now().Unix()
|
||||||
|
|
||||||
|
updatedRows, err := sess.ID(earlierTransaction.TransactionId).Cols("amount", "related_account_amount", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(earlierTransaction)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if updatedRows < 1 {
|
||||||
|
log.Errorf(c, "[transactions.MoveAllTransactionsBetweenAccounts] failed to update earlier balance modification transaction")
|
||||||
|
return errs.ErrDatabaseOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
laterTransaction.Deleted = true
|
||||||
|
laterTransaction.DeletedUnixTime = time.Now().Unix()
|
||||||
|
|
||||||
|
deletedRows, err := sess.ID(laterTransaction.TransactionId).Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(laterTransaction)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if deletedRows < 1 {
|
||||||
|
log.Errorf(c, "[transactions.MoveAllTransactionsBetweenAccounts] failed to delete later balance modification transaction")
|
||||||
|
return errs.ErrDatabaseOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof(c, "[transactions.MoveAllTransactionsBetweenAccounts] user \"uid:%d\" has combined two balance modification transactions \"id:%d\" and \"id:%d\", retained transaction is \"id:%d\"", uid, earlierTransaction.TransactionId, laterTransaction.TransactionId, earlierTransaction.TransactionId)
|
||||||
|
} else if len(balanceModificationTransactions) == 1 {
|
||||||
|
// when merging a new balance modification transaction, if its date is later than the account's earliest transaction, update the balance modification transaction time accordingly
|
||||||
|
anotherAccountId := int64(0)
|
||||||
|
|
||||||
|
if balanceModificationTransactions[0].AccountId == fromAccountId {
|
||||||
|
anotherAccountId = toAccountId
|
||||||
|
} else if balanceModificationTransactions[0].AccountId == toAccountId {
|
||||||
|
anotherAccountId = fromAccountId
|
||||||
|
} else {
|
||||||
|
log.Errorf(c, "[transactions.MoveAllTransactionsBetweenAccounts] user \"uid:%d\" has a balance modification transaction \"id:%d\" which account id is neither \"%d\" nor \"%d\"", uid, balanceModificationTransactions[0].TransactionId, fromAccountId, toAccountId)
|
||||||
|
return errs.ErrOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
earliestTransaction := &models.Transaction{}
|
||||||
|
has, err := sess.Where("uid=? AND deleted=? AND account_id=?", uid, false, anotherAccountId).OrderBy("transaction_time asc").Limit(1).Get(earliestTransaction)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if has && balanceModificationTransactions[0].TransactionTime > earliestTransaction.TransactionTime {
|
||||||
|
balanceModificationTransaction := balanceModificationTransactions[0]
|
||||||
|
balanceModificationTransaction.TransactionTime = utils.GetMinTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(earliestTransaction.TransactionTime) - 1)
|
||||||
|
balanceModificationTransaction.UpdatedUnixTime = time.Now().Unix()
|
||||||
|
|
||||||
|
if balanceModificationTransaction.TransactionTime < 0 {
|
||||||
|
balanceModificationTransaction.TransactionTime = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedRows, err := sess.ID(balanceModificationTransaction.TransactionId).Cols("transaction_time", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(balanceModificationTransaction)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if updatedRows < 1 {
|
||||||
|
log.Errorf(c, "[transactions.MoveAllTransactionsBetweenAccounts] failed to update balance modification transaction time")
|
||||||
|
return errs.ErrDatabaseOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof(c, "[transactions.MoveAllTransactionsBetweenAccounts] user \"uid:%d\" has updated balance modification transaction \"id:%d\" time to %d, because earliest transaction time in account \"id:%d\" is %d", uid, balanceModificationTransaction.TransactionId, balanceModificationTransaction.TransactionTime, toAccountId, earliestTransaction.TransactionTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update all transactions of from account
|
||||||
|
updateModel := &models.Transaction{
|
||||||
|
AccountId: toAccountId,
|
||||||
|
UpdatedUnixTime: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedRows, err := sess.Cols("account_id", "updated_unix_time").Where("uid=? AND deleted=? AND account_id=?", uid, false, fromAccountId).Update(updateModel)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatedRows > 0 {
|
||||||
|
log.Infof(c, "[transactions.MoveAllTransactionsBetweenAccounts] user \"uid:%d\" has moved %d transactions from account \"id:%d\" to account \"id:%d\"", uid, updatedRows, fromAccountId, toAccountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update all related transactions of from account
|
||||||
|
updateRelatedModel := &models.Transaction{
|
||||||
|
RelatedAccountId: toAccountId,
|
||||||
|
UpdatedUnixTime: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
relatedUpdatedRows, err := sess.Cols("related_account_id", "updated_unix_time").Where("uid=? AND deleted=? AND related_account_id=?", uid, false, fromAccountId).Update(updateRelatedModel)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatedRows > 0 {
|
||||||
|
log.Infof(c, "[transactions.MoveAllTransactionsBetweenAccounts] user \"uid:%d\" has moved %d related transactions from account \"id:%d\" to account \"id:%d\"", uid, relatedUpdatedRows, fromAccountId, toAccountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete all transfer transactions which related account id and account id are both
|
||||||
|
deletedModel := &models.Transaction{
|
||||||
|
Deleted: true,
|
||||||
|
DeletedUnixTime: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedRows, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=? AND (type=? OR type=?) AND account_id=? AND related_account_id=?", uid, false, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, models.TRANSACTION_DB_TYPE_TRANSFER_IN, toAccountId, toAccountId).Update(deletedModel)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if deletedRows > 0 {
|
||||||
|
log.Infof(c, "[transactions.MoveAllTransactionsBetweenAccounts] user \"uid:%d\" has deleted %d transactions which account id and related account id are both \"%d\"", uid, deletedRows, toAccountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update account balance
|
||||||
|
if fromAccount.Balance != 0 {
|
||||||
|
toAccount.UpdatedUnixTime = time.Now().Unix()
|
||||||
|
updatedRows, err := sess.ID(toAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", fromAccount.Balance)).Cols("updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(toAccount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if updatedRows < 1 {
|
||||||
|
log.Errorf(c, "[transactions.MoveAllTransactionsBetweenAccounts] failed to update to account balance")
|
||||||
|
return errs.ErrDatabaseOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof(c, "[transactions.MoveAllTransactionsBetweenAccounts] user \"uid:%d\" has updated account \"id:%d\" balance from %d to %d", uid, toAccountId, toAccount.Balance, toAccount.Balance+fromAccount.Balance)
|
||||||
|
|
||||||
|
fromAccount.Balance = 0
|
||||||
|
fromAccount.UpdatedUnixTime = time.Now().Unix()
|
||||||
|
updatedRows, err = sess.ID(fromAccount.AccountId).Cols("balance", "updated_unix_time").Where("uid=? AND deleted=?", fromAccount.Uid, false).Update(fromAccount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if updatedRows < 1 {
|
||||||
|
log.Errorf(c, "[transactions.MoveAllTransactionsBetweenAccounts] failed to update from account balance")
|
||||||
|
return errs.ErrDatabaseOperationFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteTransaction deletes an existed transaction from database
|
// DeleteTransaction deletes an existed transaction from database
|
||||||
func (s *TransactionService) DeleteTransaction(c core.Context, uid int64, transactionId int64) error {
|
func (s *TransactionService) DeleteTransaction(c core.Context, uid int64, transactionId int64) error {
|
||||||
if uid <= 0 {
|
if uid <= 0 {
|
||||||
@@ -1583,8 +1922,8 @@ func (s *TransactionService) GetAccountsTotalIncomeAndExpense(c core.Context, ui
|
|||||||
return incomeAmounts, expenseAmounts, nil
|
return incomeAmounts, expenseAmounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccountsAndCategoriesTotalIncomeAndExpense returns the every accounts and categories total income and expense amount by specific date range
|
// GetAccountsAndCategoriesTotalInflowAndOutflow returns the every accounts and categories total inflows and outflows amount by specific date range
|
||||||
func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c core.Context, uid int64, startUnixTime int64, endUnixTime int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, keyword string, utcOffset int16, useTransactionTimezone bool) ([]*models.Transaction, error) {
|
func (s *TransactionService) GetAccountsAndCategoriesTotalInflowAndOutflow(c core.Context, uid int64, startUnixTime int64, endUnixTime int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, keyword string, utcOffset int16, useTransactionTimezone bool) ([]*models.Transaction, error) {
|
||||||
if uid <= 0 {
|
if uid <= 0 {
|
||||||
return nil, errs.ErrUserIdInvalid
|
return nil, errs.ErrUserIdInvalid
|
||||||
}
|
}
|
||||||
@@ -1604,12 +1943,14 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c cor
|
|||||||
endTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(endUnixTime)
|
endTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(endUnixTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
condition := "uid=? AND deleted=? AND (type=? OR type=?)"
|
condition := "uid=? AND deleted=? AND (type=? OR type=? OR type=? OR type=?)"
|
||||||
conditionParams := make([]any, 0, 4)
|
conditionParams := make([]any, 0, 6)
|
||||||
conditionParams = append(conditionParams, uid)
|
conditionParams = append(conditionParams, uid)
|
||||||
conditionParams = append(conditionParams, false)
|
conditionParams = append(conditionParams, false)
|
||||||
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
|
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
|
||||||
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
|
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)
|
||||||
|
|
||||||
minTransactionTime := startTransactionTime
|
minTransactionTime := startTransactionTime
|
||||||
maxTransactionTime := endTransactionTime
|
maxTransactionTime := endTransactionTime
|
||||||
@@ -1637,7 +1978,7 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c cor
|
|||||||
finalConditionParams = append(finalConditionParams, "%%"+keyword+"%%")
|
finalConditionParams = append(finalConditionParams, "%%"+keyword+"%%")
|
||||||
}
|
}
|
||||||
|
|
||||||
sess := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...)
|
sess := s.UserDataDB(uid).NewSession(c).Select("type, category_id, account_id, related_account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...)
|
||||||
sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType)
|
sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType)
|
||||||
|
|
||||||
err := sess.Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions)
|
err := sess.Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions)
|
||||||
@@ -1673,13 +2014,20 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c cor
|
|||||||
}
|
}
|
||||||
|
|
||||||
groupKey := fmt.Sprintf("%d_%d", transaction.CategoryId, transaction.AccountId)
|
groupKey := fmt.Sprintf("%d_%d", transaction.CategoryId, transaction.AccountId)
|
||||||
|
|
||||||
|
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||||
|
groupKey = fmt.Sprintf("%d_%d_%d_%d", transaction.CategoryId, transaction.AccountId, transaction.RelatedAccountId, transaction.Type)
|
||||||
|
}
|
||||||
|
|
||||||
totalAmounts, exists := transactionTotalAmountsMap[groupKey]
|
totalAmounts, exists := transactionTotalAmountsMap[groupKey]
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
totalAmounts = &models.Transaction{
|
totalAmounts = &models.Transaction{
|
||||||
CategoryId: transaction.CategoryId,
|
Type: transaction.Type,
|
||||||
AccountId: transaction.AccountId,
|
CategoryId: transaction.CategoryId,
|
||||||
Amount: 0,
|
AccountId: transaction.AccountId,
|
||||||
|
RelatedAccountId: transaction.RelatedAccountId,
|
||||||
|
Amount: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionTotalAmountsMap[groupKey] = totalAmounts
|
transactionTotalAmountsMap[groupKey] = totalAmounts
|
||||||
@@ -1697,8 +2045,8 @@ func (s *TransactionService) GetAccountsAndCategoriesTotalIncomeAndExpense(c cor
|
|||||||
return transactionTotalAmounts, nil
|
return transactionTotalAmounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccountsAndCategoriesMonthlyIncomeAndExpense returns the every accounts monthly income and expense amount by specific date range
|
// GetAccountsAndCategoriesMonthlyInflowAndOutflow returns the every accounts monthly inflows and outflows amount by specific date range
|
||||||
func (s *TransactionService) GetAccountsAndCategoriesMonthlyIncomeAndExpense(c core.Context, uid int64, startYear int32, startMonth int32, endYear int32, endMonth int32, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, keyword string, utcOffset int16, useTransactionTimezone bool) (map[int32][]*models.Transaction, error) {
|
func (s *TransactionService) GetAccountsAndCategoriesMonthlyInflowAndOutflow(c core.Context, uid int64, startYear int32, startMonth int32, endYear int32, endMonth int32, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, keyword string, utcOffset int16, useTransactionTimezone bool) (map[int32][]*models.Transaction, error) {
|
||||||
if uid <= 0 {
|
if uid <= 0 {
|
||||||
return nil, errs.ErrUserIdInvalid
|
return nil, errs.ErrUserIdInvalid
|
||||||
}
|
}
|
||||||
@@ -1723,12 +2071,14 @@ func (s *TransactionService) GetAccountsAndCategoriesMonthlyIncomeAndExpense(c c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
condition := "uid=? AND deleted=? AND (type=? OR type=?)"
|
condition := "uid=? AND deleted=? AND (type=? OR type=? OR type=? OR type=?)"
|
||||||
conditionParams := make([]any, 0, 4)
|
conditionParams := make([]any, 0, 6)
|
||||||
conditionParams = append(conditionParams, uid)
|
conditionParams = append(conditionParams, uid)
|
||||||
conditionParams = append(conditionParams, false)
|
conditionParams = append(conditionParams, false)
|
||||||
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
|
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_INCOME)
|
||||||
conditionParams = append(conditionParams, models.TRANSACTION_DB_TYPE_EXPENSE)
|
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)
|
||||||
|
|
||||||
minTransactionTime := startTransactionTime
|
minTransactionTime := startTransactionTime
|
||||||
maxTransactionTime := endTransactionTime
|
maxTransactionTime := endTransactionTime
|
||||||
@@ -1756,7 +2106,7 @@ func (s *TransactionService) GetAccountsAndCategoriesMonthlyIncomeAndExpense(c c
|
|||||||
finalConditionParams = append(finalConditionParams, "%%"+keyword+"%%")
|
finalConditionParams = append(finalConditionParams, "%%"+keyword+"%%")
|
||||||
}
|
}
|
||||||
|
|
||||||
sess := s.UserDataDB(uid).NewSession(c).Select("category_id, account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...)
|
sess := s.UserDataDB(uid).NewSession(c).Select("type, category_id, account_id, related_account_id, transaction_time, timezone_utc_offset, amount").Where(finalCondition, finalConditionParams...)
|
||||||
sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType)
|
sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagIds, noTags, tagFilterType)
|
||||||
|
|
||||||
err := sess.Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions)
|
err := sess.Limit(pageCountForLoadTransactionAmounts, 0).OrderBy("transaction_time desc").Find(&transactions)
|
||||||
@@ -1795,13 +2145,22 @@ func (s *TransactionService) GetAccountsAndCategoriesMonthlyIncomeAndExpense(c c
|
|||||||
}
|
}
|
||||||
|
|
||||||
groupKey := fmt.Sprintf("%d_%d_%d", yearMonth, transaction.CategoryId, transaction.AccountId)
|
groupKey := fmt.Sprintf("%d_%d_%d", yearMonth, transaction.CategoryId, transaction.AccountId)
|
||||||
|
|
||||||
|
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||||
|
groupKey = fmt.Sprintf("%d_%d_%d_%d_%d", yearMonth, transaction.CategoryId, transaction.AccountId, transaction.RelatedAccountId, transaction.Type)
|
||||||
|
}
|
||||||
|
|
||||||
transactionAmounts, exists := transactionsMonthlyAmountsMap[groupKey]
|
transactionAmounts, exists := transactionsMonthlyAmountsMap[groupKey]
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
transactionAmounts = &models.Transaction{
|
transactionAmounts = &models.Transaction{
|
||||||
CategoryId: transaction.CategoryId,
|
Type: transaction.Type,
|
||||||
AccountId: transaction.AccountId,
|
CategoryId: transaction.CategoryId,
|
||||||
|
AccountId: transaction.AccountId,
|
||||||
|
RelatedAccountId: transaction.RelatedAccountId,
|
||||||
|
Amount: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionsMonthlyAmountsMap[groupKey] = transactionAmounts
|
transactionsMonthlyAmountsMap[groupKey] = transactionAmounts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user