mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Compare commits
245 Commits
Author | SHA1 | Date | |
---|---|---|---|
0160a9ae52 | |||
9751f8d11d | |||
a1048d480a | |||
8ccfcef8e3 | |||
934178412e | |||
9d384e5d1c | |||
1615a35628 | |||
2e0e791853 | |||
97a10cc08a | |||
3922c3767d | |||
efbfd5c231 | |||
91b89645ae | |||
c0a0da2170 | |||
41311a8b06 | |||
0c51d8682b | |||
0cb1afc5b5 | |||
75cc071222 | |||
e8310cfa69 | |||
746608c062 | |||
a1444bca8c | |||
ef5b49ebd8 | |||
fb5b2f52c7 | |||
a49abfe0de | |||
cf5a515952 | |||
c1f1b05fa8 | |||
9166c98df7 | |||
bfd2832846 | |||
814e74a41e | |||
f755275309 | |||
731598fa38 | |||
8e521741f8 | |||
3aac5e9062 | |||
50c54685ec | |||
dc0bcbe65d | |||
bafbc34706 | |||
8ca1404f8b | |||
195755581b | |||
8a94fef06b | |||
ebcf87ea93 | |||
0e83ab02fa | |||
05ea05cdf4 | |||
f39ecc46bd | |||
333c1b5dd0 | |||
52d45d4644 | |||
f46f24f0be | |||
9cce0ac2e1 | |||
497046d0a4 | |||
03af194385 | |||
ad704cef5c | |||
cd5c511474 | |||
8a26e24081 | |||
db6dde32cd | |||
394215e53b | |||
27586f3a54 | |||
9e9e9fbef9 | |||
bc9132f84d | |||
e7b6a87153 | |||
0a2cba647c | |||
f5395e36ad | |||
9f38246fe2 | |||
5242df2b7d | |||
10648d66ad | |||
97fab3e109 | |||
0f3b41c2dd | |||
5ae7527b7b | |||
2db065d47a | |||
0e5c5a56d2 | |||
a4b89d3a69 | |||
aab9e98ebd | |||
d4945c982f | |||
964405f349 | |||
21f6809f05 | |||
c5fda02900 | |||
10e432c185 | |||
f121112d09 | |||
c13fc96a16 | |||
61f13fce20 | |||
99e50b1062 | |||
4ce75c2acb | |||
fcca881cfc | |||
e2ef54152d | |||
ebe1836ac6 | |||
b1a12a5759 | |||
ae407fffca | |||
94e0d06e5d | |||
088bd17803 | |||
2976203ecc | |||
e75bd94531 | |||
4cc8c21f67 | |||
f182b804bb | |||
e89ce076fd | |||
ba81c07345 | |||
9586dbf781 | |||
c8ea1a503f | |||
ebbc21f0b1 | |||
6e5bc38e5e | |||
9424c49760 | |||
efd6ba36e3 | |||
b1d7f87095 | |||
ffbcfc7467 | |||
41f6db8f34 | |||
8a21be4306 | |||
31ca4a1e02 | |||
7cab2b0be7 | |||
777997c883 | |||
060a33263a | |||
33d259592c | |||
fbae5f8757 | |||
bc99dc990a | |||
1e9d3f9e80 | |||
2ce720c20f | |||
ef87445e43 | |||
dec5849661 | |||
5609c0ada3 | |||
1632cea949 | |||
23759d526a | |||
82a565738f | |||
1989a69926 | |||
7a07c9d4fc | |||
a27fe04919 | |||
1d7ff4bc2a | |||
b3fa032bde | |||
94377a8dea | |||
dba4da8641 | |||
4a22a19cb0 | |||
13a3d9f03a | |||
beffe71ea6 | |||
0ab7faf7b6 | |||
a2ac049578 | |||
b287c4ca36 | |||
018cc50fb8 | |||
1d4156bdfe | |||
147c79db60 | |||
f204ca888d | |||
e28070b288 | |||
4d217a83c1 | |||
9e0581b311 | |||
ffb529f4cf | |||
c9aac2a273 | |||
dd8658e33e | |||
e399af1f1f | |||
4c1f4ed39b | |||
7e5c00d0ae | |||
cec2a84e2d | |||
ffb0b84d78 | |||
8a7333b899 | |||
dd3b9c9b9c | |||
d2b62e21c5 | |||
9505773165 | |||
4bfc8a9e9f | |||
df5fe6e623 | |||
037ad7b9b1 | |||
ec10cc922c | |||
acb76e1ab1 | |||
252a304ba8 | |||
c863cf6dc5 | |||
373d969734 | |||
99a3e8f5da | |||
4302cfcbd6 | |||
7cca0055fe | |||
20993a1182 | |||
d5a85639b1 | |||
b6a8185957 | |||
c5da5e4622 | |||
a0f69a371f | |||
2f0cb112dd | |||
2173954b84 | |||
991e64b961 | |||
affff0c386 | |||
099cdaddbc | |||
409405117e | |||
af89ecc9c1 | |||
be354fa790 | |||
a1c4c5da6b | |||
33509beaf7 | |||
ab6ccbdfbe | |||
77e6cd9faa | |||
34bc38cecf | |||
69d3e0494b | |||
a3136ebb13 | |||
4a4d0dad4b | |||
3b87511f48 | |||
f5fba04097 | |||
ad566993ad | |||
5f1e498454 | |||
2e0f79df3b | |||
4a4e19fcbd | |||
45d4ba89f5 | |||
29b3e619ca | |||
1a85ebc0f7 | |||
4bd58789f4 | |||
09d1124794 | |||
41584bdd82 | |||
1b7baf6fc9 | |||
a76db3e95f | |||
74a5226e73 | |||
d245b1e5d0 | |||
d5eff46651 | |||
30a65b4de9 | |||
9048a8eb7a | |||
1f19c5e93c | |||
4b0a3cf0d6 | |||
d778612242 | |||
ff7d595a86 | |||
9d7688957f | |||
179042f81b | |||
e6441f124c | |||
15c391d1d4 | |||
91c765202c | |||
5276f68918 | |||
e774039831 | |||
40067d252e | |||
1a47243f70 | |||
a1f6c2884b | |||
977420c68d | |||
3ae66a3898 | |||
5c5c462035 | |||
f6cc489425 | |||
5aae18e241 | |||
8a731a252a | |||
8fc0d78f64 | |||
a675417ab9 | |||
408d9086e7 | |||
8f933d8648 | |||
bbc85de34b | |||
ec70d024fa | |||
eae45baf38 | |||
4cea50b5c8 | |||
e4814431e0 | |||
91b4cb2c13 | |||
a3acdc7041 | |||
e7e5254673 | |||
8e558d8dee | |||
b763c4acc6 | |||
d1bd7b96b8 | |||
8c65da9031 | |||
647bf1781d | |||
85515d6cb5 | |||
1258ec0438 | |||
965d8e22b3 | |||
ed6e51b4df | |||
af879f8d57 | |||
f15efcd6f2 | |||
22e91ad362 | |||
932ba111cc |
124
.github/workflows/ci.yml
vendored
Normal file
124
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,124 @@
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: 'Unit- & API tests'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.19
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get dependencies
|
||||
run: go get
|
||||
|
||||
- name: Unit Tests
|
||||
run: CGO_ENABLED=0 go test `go list ./... | grep -v 'github.com/muety/wakapi/scripts'` -run ./... # skip scripts package, because not actually a package
|
||||
|
||||
- name: API Tests
|
||||
run: |
|
||||
npm -g install newman
|
||||
./testing/run_api_tests.sh
|
||||
|
||||
mapi:
|
||||
name: 'Automated pen-tests with Mayhem for API'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
steps:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.19
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get dependencies
|
||||
run: go get
|
||||
|
||||
- name: Build
|
||||
run: go build -v .
|
||||
|
||||
- name: start wakapi
|
||||
run: ./wakapi --config config.default.yml &
|
||||
|
||||
- name: create a trivial testing user
|
||||
run: sqlite3 wakapi_db.db "insert into users (id, api_key) values ('mapi', 'test-api-key')"
|
||||
|
||||
- name: Run Mayhem for API
|
||||
uses: ForAllSecure/mapi-action@v1
|
||||
continue-on-error: true
|
||||
with:
|
||||
mapi-token: ${{ secrets.MAPI_TOKEN }}
|
||||
api-url: http://localhost:3000/api/
|
||||
api-spec: static/docs/swagger.yaml
|
||||
target: muety/wakapi
|
||||
duration: 1min
|
||||
sarif-report: mapi.sarif
|
||||
run-args: |
|
||||
--header-auth
|
||||
Authorization: Basic dGVzdC1hcGkta2V5
|
||||
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
with:
|
||||
sarif_file: mapi.sarif
|
||||
|
||||
build:
|
||||
name: 'Build (Win, Linux, Mac)'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
steps:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.19
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get dependencies
|
||||
run: go get
|
||||
|
||||
- name: Build
|
||||
run: go build -v .
|
||||
|
||||
migration:
|
||||
name: Migration tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
db: [sqlite, postgres, mysql, mariadb]
|
||||
|
||||
steps:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.19
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- run: ./testing/run_api_tests.sh ${{ matrix.db }} --migration
|
11
.github/workflows/docker.yml
vendored
11
.github/workflows/docker.yml
vendored
@ -8,8 +8,18 @@ on:
|
||||
|
||||
jobs:
|
||||
docker-publish:
|
||||
name: 'Build and publish Docker image'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set version
|
||||
run: |
|
||||
(git describe --tags --exact-match \
|
||||
|| git symbolic-ref -q --short HEAD \
|
||||
|| git rev-parse --short HEAD) > version.txt 2> /dev/null
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
@ -46,6 +56,7 @@ jobs:
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
|
55
.github/workflows/linux-build-on-release.yml
vendored
55
.github/workflows/linux-build-on-release.yml
vendored
@ -1,55 +0,0 @@
|
||||
name: Linux
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
pull_request:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
name: Linux - Build, Test & Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get dependencies
|
||||
run: go get
|
||||
|
||||
- name: Unit Tests
|
||||
run: go test ./... -run ./...
|
||||
|
||||
- name: API Tests
|
||||
run: |
|
||||
npm -g install newman
|
||||
./testing/run_api_tests.sh
|
||||
|
||||
- name: Build
|
||||
run: GO111MODULE=on go build -v .
|
||||
|
||||
- name: Zip executable and sample config
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
cp config.default.yml config.yml
|
||||
zip -9 release.zip wakapi config.yml
|
||||
|
||||
- name: Upload built executable to Release
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: release.zip
|
||||
asset_name: wakapi_linux_amd64.zip
|
||||
asset_content_type: application/gzip
|
77
.github/workflows/release.yml
vendored
Normal file
77
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,77 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: 'Build, package and release to GitHub'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: ubuntu-18.04
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
- platform: ubuntu-18.04
|
||||
GOOS: linux
|
||||
GOARCH: arm64
|
||||
- platform: windows-latest
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
- platform: macos-latest
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
- platform: macos-latest
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.19
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set version
|
||||
shell: bash
|
||||
run: |
|
||||
(git describe --tags --exact-match \
|
||||
|| git symbolic-ref -q --short HEAD \
|
||||
|| git rev-parse --short HEAD) > version.txt 2> /dev/null
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
mkdir -p dist/ && cd dist/
|
||||
cp ../config.default.yml config.yml
|
||||
|
||||
- name: Build
|
||||
working-directory: ./dist
|
||||
shell: bash
|
||||
run: |
|
||||
GOOS=${{ matrix.GOOS }} GOARCH=${{ matrix.GOARCH }} CGO_ENABLED=0 \
|
||||
go build -v -ldflags '-w -s' ../
|
||||
|
||||
- name: Compress working folder (Windows PowerShell)
|
||||
working-directory: ./dist
|
||||
if: "${{ matrix.GOOS == 'windows' }}"
|
||||
run: |
|
||||
Compress-Archive -Path .\wakapi.exe, .\config.yml -DestinationPath wakapi_${{ matrix.GOOS }}_${{ matrix.GOARCH }}.zip
|
||||
|
||||
- name: Compress working folder
|
||||
working-directory: ./dist
|
||||
if: "${{ matrix.GOOS != 'windows' }}"
|
||||
run: |
|
||||
zip -9 wakapi_${{ matrix.GOOS }}_${{ matrix.GOARCH }}.zip *
|
||||
|
||||
- name: Upload built executable to Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ./dist/*.zip
|
51
.github/workflows/win-build-on-release.yml
vendored
51
.github/workflows/win-build-on-release.yml
vendored
@ -1,51 +0,0 @@
|
||||
name: Windows
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
pull_request:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
name: Windows - Build & Release
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get
|
||||
|
||||
- name: Enable Go 1.11 modules
|
||||
run: cmd /c "set GO111MODULE=on"
|
||||
|
||||
- name: Build
|
||||
run: go build -v .
|
||||
|
||||
- name: Compress working folder
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
cp .\config.default.yml .\config.yml
|
||||
Compress-Archive -Path .\wakapi.exe, .\config.yml -DestinationPath release.zip
|
||||
|
||||
- name: Upload built executable to Release
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: release.zip
|
||||
asset_name: wakapi_win_amd64.zip
|
||||
asset_content_type: application/gzip
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -5,9 +5,10 @@ wakapi
|
||||
build
|
||||
*.exe
|
||||
*.db
|
||||
*.zip
|
||||
config*.yml
|
||||
!config.default.yml
|
||||
!testing/config.testing.yml
|
||||
!testing/config.*.yml
|
||||
pkged.go
|
||||
package-lock.json
|
||||
node_modules
|
||||
node_modules
|
||||
|
59
Dockerfile
59
Dockerfile
@ -1,33 +1,22 @@
|
||||
# To build locally: docker buildx build . -t wakapi --load
|
||||
|
||||
# Preparation to save some time
|
||||
FROM --platform=$BUILDPLATFORM golang:1.17-alpine AS prep-env
|
||||
FROM golang:1.19-alpine AS build-env
|
||||
WORKDIR /src
|
||||
|
||||
ADD ./go.mod .
|
||||
RUN go mod download
|
||||
ADD . .
|
||||
|
||||
RUN wget "https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh" -O wait-for-it.sh && \
|
||||
chmod +x wait-for-it.sh
|
||||
|
||||
# Build Stage
|
||||
FROM golang:1.17-alpine AS build-env
|
||||
ADD ./go.mod ./go.sum ./
|
||||
RUN go mod download
|
||||
ADD . .
|
||||
|
||||
# Required for go-sqlite3
|
||||
RUN apk add --no-cache gcc musl-dev
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -v -o wakapi main.go
|
||||
|
||||
WORKDIR /src
|
||||
COPY --from=prep-env /src .
|
||||
|
||||
RUN go build -v -o wakapi
|
||||
|
||||
WORKDIR /app
|
||||
RUN cp /src/wakapi . && \
|
||||
cp /src/config.default.yml config.yml && \
|
||||
sed -i 's/listen_ipv6: ::1/listen_ipv6: /g' config.yml && \
|
||||
cp /src/wait-for-it.sh . && \
|
||||
cp /src/entrypoint.sh .
|
||||
WORKDIR /staging
|
||||
RUN mkdir ./data ./app && \
|
||||
cp /src/wakapi app/ && \
|
||||
cp /src/config.default.yml app/config.yml && \
|
||||
sed -i 's/listen_ipv6: ::1/listen_ipv6: /g' app/config.yml && \
|
||||
cp /src/wait-for-it.sh app/ && \
|
||||
cp /src/entrypoint.sh app/
|
||||
|
||||
# Run Stage
|
||||
|
||||
@ -41,20 +30,18 @@ WORKDIR /app
|
||||
RUN apk add --no-cache bash ca-certificates tzdata
|
||||
|
||||
# See README.md and config.default.yml for all config options
|
||||
ENV ENVIRONMENT prod
|
||||
ENV WAKAPI_DB_TYPE sqlite3
|
||||
ENV WAKAPI_DB_USER ''
|
||||
ENV WAKAPI_DB_PASSWORD ''
|
||||
ENV WAKAPI_DB_HOST ''
|
||||
ENV WAKAPI_DB_NAME=/data/wakapi.db
|
||||
ENV WAKAPI_PASSWORD_SALT ''
|
||||
ENV WAKAPI_LISTEN_IPV4 '0.0.0.0'
|
||||
ENV WAKAPI_INSECURE_COOKIES 'true'
|
||||
ENV WAKAPI_ALLOW_SIGNUP 'true'
|
||||
ENV ENVIRONMENT=prod \
|
||||
WAKAPI_DB_TYPE=sqlite3 \
|
||||
WAKAPI_DB_USER='' \
|
||||
WAKAPI_DB_PASSWORD='' \
|
||||
WAKAPI_DB_HOST='' \
|
||||
WAKAPI_DB_NAME=/data/wakapi.db \
|
||||
WAKAPI_PASSWORD_SALT='' \
|
||||
WAKAPI_LISTEN_IPV4='0.0.0.0' \
|
||||
WAKAPI_INSECURE_COOKIES='true' \
|
||||
WAKAPI_ALLOW_SIGNUP='true'
|
||||
|
||||
COPY --from=build-env /app .
|
||||
|
||||
VOLUME /data
|
||||
COPY --from=build-env /staging /
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
183
README.md
183
README.md
@ -6,7 +6,7 @@
|
||||
<img src="https://badges.fw-web.space/github/license/muety/wakapi">
|
||||
<a href="#-treeware"><img src="https://badges.fw-web.space:/treeware/trees/muety/wakapi?color=%234EC820&label=%F0%9F%8C%B3%20trees"></a>
|
||||
<a href="https://liberapay.com/muety/"><img src="https://badges.fw-web.space/liberapay/receives/muety.svg?logo=liberapay"></a>
|
||||
<img src="https://badges.fw-web.space/endpoint?url=https://wakapi.dev/api/compat/shields/v1/n1try/interval:any/project:wakapi&color=blue&label=wakapi">
|
||||
<img src="https://wakapi.dev/api/badge/n1try/interval:any/project:wakapi?label=wakapi">
|
||||
<img src="https://badges.fw-web.space/github/languages/code-size/muety/wakapi">
|
||||
<a href="https://goreportcard.com/report/github.com/muety/wakapi"><img src="https://goreportcard.com/badge/github.com/muety/wakapi"></a>
|
||||
<a href="https://sonarcloud.io/dashboard?id=muety_wakapi"><img src="https://sonarcloud.io/api/project_badges/measure?project=muety_wakapi&metric=ncloc"></a>
|
||||
@ -35,11 +35,12 @@
|
||||
Installation instructions can be found below and in the [Wiki](https://github.com/muety/wakapi/wiki).
|
||||
|
||||
## 🚀 Features
|
||||
* ✅ 100 % free and open-source
|
||||
|
||||
* ✅ Free and open-source
|
||||
* ✅ Built by developers for developers
|
||||
* ✅ Statistics for projects, languages, editors, hosts and operating systems
|
||||
* ✅ Badges
|
||||
* ✅ Weekly E-Mail Reports
|
||||
* ✅ Weekly E-Mail reports
|
||||
* ✅ REST API
|
||||
* ✅ Partially compatible with WakaTime
|
||||
* ✅ WakaTime integration
|
||||
@ -48,20 +49,30 @@ Installation instructions can be found below and in the [Wiki](https://github.co
|
||||
* ✅ Self-hosted
|
||||
|
||||
## 🚧 Roadmap
|
||||
Plans for the near future mainly include, besides usual improvements and bug fixes, a UI redesign as well as additional types of charts and statistics (see [#101](https://github.com/muety/wakapi/issues/101), [#76](https://github.com/muety/wakapi/issues/76), [#12](https://github.com/muety/wakapi/issues/12)). If you have feature requests or any kind of improvement proposals feel free to open an issue or share them in our [user survey](https://github.com/muety/wakapi/issues/82).
|
||||
|
||||
Plans for the near future mainly include, besides usual improvements and bug fixes, a UI redesign as well as additional types of charts and statistics (see [#101](https://github.com/muety/wakapi/issues/101), [#76](https://github.com/muety/wakapi/issues/76), [#12](https://github.com/muety/wakapi/issues/12)). If you have feature requests or any kind of improvement proposals feel free to open an issue or share them in our [user survey](https://github.com/muety/wakapi/issues/82).
|
||||
|
||||
## ⌨️ How to use?
|
||||
There are different options for how to use Wakapi, ranging from our hosted cloud service to self-hosting it. Regardless of which option choose, you will always have to do the [client setup](#-client-setup) in addition.
|
||||
|
||||
There are different options for how to use Wakapi, ranging from our hosted cloud service to self-hosting it. Regardless of which option choose, you will always have to do the [client setup](#-client-setup) in addition.
|
||||
|
||||
### ☁️ Option 1: Use [wakapi.dev](https://wakapi.dev)
|
||||
|
||||
If you want to try out a free, hosted cloud service, all you need to do is create an account and then set up your client-side tooling (see below).
|
||||
|
||||
### 📦 Option 2: Quick-run a Release
|
||||
### 📦 Option 2: Quick-run a release
|
||||
|
||||
```bash
|
||||
$ curl -L https://wakapi.dev/get | bash
|
||||
```
|
||||
|
||||
**Alternatively** using [eget](https://github.com/zyedidia/eget):
|
||||
```bash
|
||||
$ eget muety/wakapi
|
||||
```
|
||||
|
||||
### 🐳 Option 3: Use Docker
|
||||
|
||||
```bash
|
||||
# Create a persistent volume
|
||||
$ docker volume create wakapi-data
|
||||
@ -82,39 +93,36 @@ $ docker run -d \
|
||||
If you want to run Wakapi on **Kubernetes**, there is [wakapi-helm-chart](https://github.com/andreymaznyak/wakapi-helm-chart) for quick and easy deployment.
|
||||
|
||||
### 🧑💻 Option 4: Compile and run from source
|
||||
#### Prerequisites
|
||||
* Go >= 1.16 (with `$GOPATH` properly set)
|
||||
* gcc (to compile [go-sqlite3](https://github.com/mattn/go-sqlite3))
|
||||
* Fedora / RHEL: `dnf install @development-tools`
|
||||
* Ubuntu / Debian: `apt install build-essential`
|
||||
* Windows: See [here](https://github.com/mattn/go-sqlite3/issues/214#issuecomment-253216476)
|
||||
|
||||
#### Compile & Run
|
||||
```bash
|
||||
# Build the executable
|
||||
$ go build -o wakapi
|
||||
# Build and install
|
||||
# Alternatively: go build -o wakapi
|
||||
$ go install github.com/muety/wakapi@latest
|
||||
|
||||
# Adapt config to your needs
|
||||
$ cp config.default.yml config.yml
|
||||
$ vi config.yml
|
||||
# Get default config and customize
|
||||
$ curl -o wakapi.yml https://raw.githubusercontent.com/muety/wakapi/master/config.default.yml
|
||||
$ vi wakapi.yml
|
||||
|
||||
# Run it
|
||||
$ ./wakapi
|
||||
$ ./wakapi -config wakapi.yml
|
||||
```
|
||||
|
||||
**Note:** Check the comments `config.yml` for best practices regarding security configuration and more.
|
||||
**Note:** Check the comments in `config.yml` for best practices regarding security configuration and more.
|
||||
|
||||
💡 When running Wakapi standalone (without Docker), it is recommended to run it as a [SystemD service](etc/wakapi.service).
|
||||
|
||||
### 💻 Client setup
|
||||
|
||||
### 💻 Client Setup
|
||||
Wakapi relies on the open-source [WakaTime](https://github.com/wakatime/wakatime) client tools. In order to collect statistics for Wakapi, you need to set them up.
|
||||
|
||||
1. **Set up WakaTime** for your specific IDE or editor. Please refer to the respective [plugin guide](https://wakatime.com/plugins)
|
||||
2. **Editing your local `~/.wakatime.cfg`** file as follows
|
||||
2. **Edit your local `~/.wakatime.cfg`** file as follows.
|
||||
|
||||
```ini
|
||||
[settings]
|
||||
|
||||
# Your Wakapi server URL or 'https://wakapi.dev' when using the cloud server
|
||||
api_url = http://localhost:3000/api/heartbeat
|
||||
# Your Wakapi server URL or 'https://wakapi.dev/api' when using the cloud server
|
||||
api_url = http://localhost:3000/api
|
||||
|
||||
# Your Wakapi API key (get it from the web interface after having created an account)
|
||||
api_key = 406fe41f-6d69-4183-a4cc-121e0c524c2b
|
||||
@ -122,22 +130,34 @@ api_key = 406fe41f-6d69-4183-a4cc-121e0c524c2b
|
||||
|
||||
Optionally, you can set up a [client-side proxy](https://github.com/muety/wakapi/wiki/Advanced-Setup:-Client-side-proxy) in addition.
|
||||
|
||||
## 🔧 Configuration Options
|
||||
## 🔧 Configuration options
|
||||
|
||||
You can specify configuration options either via a config file (default: `config.yml`, customizable through the `-c` argument) or via environment variables. Here is an overview of all options.
|
||||
|
||||
| YAML Key / Env. Variable | Default | Description |
|
||||
| YAML key / Env. variable | Default | Description |
|
||||
|------------------------------------------------------------------------------|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `env` /<br>`ENVIRONMENT` | `dev` | Whether to use development- or production settings |
|
||||
| `app.aggregation_time` /<br>`WAKAPI_AGGREGATION_TIME` | `0 15 2 * * *` | Time of day at which to periodically run summary generation for all users |
|
||||
| `app.report_time_weekly` /<br>`WAKAPI_REPORT_TIME_WEEKLY` | `0 0 18 * * 5` | Week day and time at which to send e-mail reports |
|
||||
| `app.leaderboard_generation_time` /<br>`WAKAPI_LEADERBOARD_GENERATION_TIME` | `0 0 6 * * *,0 0 18 * * *` | One or multiple times of day at which to re-calculate the leaderboard |
|
||||
| `app.data_cleanup_time` /<br>`WAKAPI_DATA_CLEANUP_TIME` | `0 0 6 * * 0` | When to perform data cleanup operations (see `app.data_retention_months`) |
|
||||
| `app.import_batch_size` /<br>`WAKAPI_IMPORT_BATCH_SIZE` | `50` | Size of batches of heartbeats to insert to the database during importing from external services |
|
||||
| `app.inactive_days` /<br>`WAKAPI_INACTIVE_DAYS` | `7` | Number of days after which to consider a user inactive (only for metrics) |
|
||||
| `app.heartbeat_max_age /`<br>`WAKAPI_HEARTBEAT_MAX_AGE` | `4320h` | Maximum acceptable age of a heartbeat (see [`ParseDuration`](https://pkg.go.dev/time#ParseDuration)) |
|
||||
| `app.custom_languages` | - | Map from file endings to language names |
|
||||
| `app.avatar_url_template` | (see [`config.default.yml`](config.default.yml)) | URL template for external user avatar images (e.g. from [Dicebear](https://dicebear.com) or [Gravatar](https://gravatar.com)) |
|
||||
| `app.avatar_url_template` /<br>`WAKAPI_AVATAR_URL_TEMPLATE` | (see [`config.default.yml`](config.default.yml)) | URL template for external user avatar images (e.g. from [Dicebear](https://dicebear.com) or [Gravatar](https://gravatar.com)) |
|
||||
| `app.support_contact` /<br>`WAKAPI_SUPPORT_CONTACT` | `hostmaster@wakapi.dev` | E-Mail address to display as a support contact on the page |
|
||||
| `app.data_retention_months` /<br>`WAKAPI_DATA_RETENTION_MONTHS` | `-1` | Maximum retention period in months for user data (heartbeats) (-1 for unlimited) |
|
||||
| `server.port` /<br> `WAKAPI_PORT` | `3000` | Port to listen on |
|
||||
| `server.listen_ipv4` /<br> `WAKAPI_LISTEN_IPV4` | `127.0.0.1` | IPv4 network address to listen on (leave blank to disable IPv4) |
|
||||
| `server.listen_ipv6` /<br> `WAKAPI_LISTEN_IPV6` | `::1` | IPv6 network address to listen on (leave blank to disable IPv6) |
|
||||
| `server.listen_socket` /<br> `WAKAPI_LISTEN_SOCKET` | - | UNIX socket to listen on (leave blank to disable UNIX socket) |
|
||||
| `server.listen_socket_mode` /<br> `WAKAPI_LISTEN_SOCKET_MODE` | `0666` | Permission mode to create UNIX socket with |
|
||||
| `server.timeout_sec` /<br> `WAKAPI_TIMEOUT_SEC` | `30` | Request timeout in seconds |
|
||||
| `server.tls_cert_path` /<br> `WAKAPI_TLS_CERT_PATH` | - | Path of SSL server certificate (leave blank to not use HTTPS) |
|
||||
| `server.tls_key_path` /<br> `WAKAPI_TLS_KEY_PATH` | - | Path of SSL server private key (leave blank to not use HTTPS) |
|
||||
| `server.base_path` /<br> `WAKAPI_BASE_PATH` | `/` | Web base path (change when running behind a proxy under a sub-path) |
|
||||
| `server.public_url` /<br> `WAKAPI_PUBLIC_URL` | `http://localhost:3000` | URL at which your Wakapi instance can be found publicly |
|
||||
| `security.password_salt` /<br> `WAKAPI_PASSWORD_SALT` | - | Pepper to use for password hashing |
|
||||
| `security.insecure_cookies` /<br> `WAKAPI_INSECURE_COOKIES` | `false` | Whether or not to allow cookies over HTTP |
|
||||
| `security.cookie_max_age` /<br> `WAKAPI_COOKIE_MAX_AGE` | `172800` | Lifetime of authentication cookies in seconds or `0` to use [Session](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Define_the_lifetime_of_a_cookie) cookies |
|
||||
@ -145,6 +165,7 @@ You can specify configuration options either via a config file (default: `config
|
||||
| `security.expose_metrics` /<br> `WAKAPI_EXPOSE_METRICS` | `false` | Whether to expose Prometheus metrics under `/api/metrics` |
|
||||
| `db.host` /<br> `WAKAPI_DB_HOST` | - | Database host |
|
||||
| `db.port` /<br> `WAKAPI_DB_PORT` | - | Database port |
|
||||
| `db.socket` /<br> `WAKAPI_DB_SOCKET` | - | Database UNIX socket (alternative to `host`) (for MySQL only) |
|
||||
| `db.user` /<br> `WAKAPI_DB_USER` | - | Database user |
|
||||
| `db.password` /<br> `WAKAPI_DB_PASSWORD` | - | Database password |
|
||||
| `db.name` /<br> `WAKAPI_DB_NAME` | `wakapi_db.db` | Database name |
|
||||
@ -154,35 +175,47 @@ You can specify configuration options either via a config file (default: `config
|
||||
| `db.ssl` /<br> `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres and CockroachDB only) |
|
||||
| `db.automgirate_fail_silently` /<br> `WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY` | `false` | Whether to ignore schema auto-migration failures when starting up |
|
||||
| `mail.enabled` /<br> `WAKAPI_MAIL_ENABLED` | `true` | Whether to allow Wakapi to send e-mail (e.g. for password resets) |
|
||||
| `mail.sender` /<br> `WAKAPI_MAIL_SENDER` | `noreply@wakapi.dev` | Default sender address for outgoing mails (ignored for MailWhale) |
|
||||
| `mail.sender` /<br> `WAKAPI_MAIL_SENDER` | `Wakapi <noreply@wakapi.dev>` | Default sender address for outgoing mails (ignored for MailWhale) |
|
||||
| `mail.provider` /<br> `WAKAPI_MAIL_PROVIDER` | `smtp` | Implementation to use for sending mails (one of [`smtp`, `mailwhale`]) |
|
||||
| `mail.smtp.*` /<br> `WAKAPI_MAIL_SMTP_*` | `-` | Various options to configure SMTP. See [default config](config.default.yml) for details |
|
||||
| `mail.mailwhale.*` /<br> `WAKAPI_MAIL_MAILWHALE_*` | `-` | Various options to configure [MailWhale](https://mailwhale.dev) sending service. See [default config](config.default.yml) for details |
|
||||
| `mail.smtp.host` /<br> `WAKAPI_MAIL_SMTP_HOST` | - | SMTP server address for sending mail (if using `smtp` mail provider) |
|
||||
| `mail.smtp.port` /<br> `WAKAPI_MAIL_SMTP_PORT` | - | SMTP server port (usually 465) |
|
||||
| `mail.smtp.username` /<br> `WAKAPI_MAIL_SMTP_USER` | - | SMTP server authentication username |
|
||||
| `mail.smtp.password` /<br> `WAKAPI_MAIL_SMTP_PASS` | - | SMTP server authentication password |
|
||||
| `mail.smtp.tls` /<br> `WAKAPI_MAIL_SMTP_TLS` | `false` | Whether the SMTP server requires TLS encryption (`false` for STARTTLS or no encryption) |
|
||||
| `mail.mailwhale.url` /<br> `WAKAPI_MAIL_MAILWHALE_URL` | - | URL of [MailWhale](https://mailwhale.dev) instance (e.g. `https://mailwhale.dev`) (if using `mailwhale` mail provider) |
|
||||
| `mail.mailwhale.client_id` /<br> `WAKAPI_MAIL_MAILWHALE_CLIENT_ID` | - | MailWhale API client ID |
|
||||
| `mail.mailwhale.client_secret` /<br> `WAKAPI_MAIL_MAILWHALE_CLIENT_SECRET` | - | MailWhale API client secret |
|
||||
| `sentry.dsn` /<br> `WAKAPI_SENTRY_DSN` | – | DSN for to integrate [Sentry](https://sentry.io) for error logging and tracing (leave empty to disable) |
|
||||
| `sentry.enable_tracing` /<br> `WAKAPI_SENTRY_TRACING` | `false` | Whether to enable Sentry request tracing |
|
||||
| `sentry.sample_rate` /<br> `WAKAPI_SENTRY_SAMPLE_RATE` | `0.75` | Probability of tracing a request in Sentry |
|
||||
| `sentry.sample_rate_heartbeats` /<br> `WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS` | `0.1` | Probability of tracing a heartbeats request in Sentry |
|
||||
| `sentry.sample_rate_heartbeats` /<br> `WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS` | `0.1` | Probability of tracing a heartbeat request in Sentry |
|
||||
| `quick_start` /<br> `WAKAPI_QUICK_START` | `false` | Whether to skip initial boot tasks. Use only for development purposes! |
|
||||
|
||||
### Supported databases
|
||||
|
||||
Wakapi uses [GORM](https://gorm.io) as an ORM. As a consequence, a set of different relational databases is supported.
|
||||
|
||||
* [SQLite](https://sqlite.org/) (_default, easy setup_)
|
||||
* [MySQL](https://hub.docker.com/_/mysql) (_recommended, because most extensively tested_)
|
||||
* [MariaDB](https://hub.docker.com/_/mariadb) (_open-source MySQL alternative_)
|
||||
* [Postgres](https://hub.docker.com/_/postgres) (_open-source as well_)
|
||||
* [CockroachDB](https://www.cockroachlabs.com/docs/stable/install-cockroachdb-linux.html) (_cloud-native, distributed, Postgres-compatible API_)
|
||||
|
||||
## 🔧 API Endpoints
|
||||
## 🔧 API endpoints
|
||||
|
||||
See our [Swagger API Documentation](https://wakapi.dev/swagger-ui).
|
||||
|
||||
### Generating Swagger docs
|
||||
|
||||
```bash
|
||||
$ go get -u github.com/swaggo/swag/cmd/swag
|
||||
$ go install github.com/swaggo/swag/cmd/swag@latest
|
||||
$ swag init -o static/docs
|
||||
```
|
||||
|
||||
## 🤝 Integrations
|
||||
### Prometheus Export
|
||||
|
||||
### Prometheus export
|
||||
|
||||
You can export your Wakapi statistics to Prometheus to view them in a Grafana dashboard or so. Here is how.
|
||||
|
||||
```bash
|
||||
@ -197,6 +230,7 @@ $ echo "<YOUR_API_KEY>" | base64
|
||||
```
|
||||
|
||||
#### Scrape config example
|
||||
|
||||
```yml
|
||||
# prometheus.yml
|
||||
# (assuming your Wakapi instance listens at localhost, port 3000)
|
||||
@ -210,15 +244,18 @@ scrape_configs:
|
||||
- targets: ['localhost:3000']
|
||||
```
|
||||
|
||||
#### Grafana
|
||||
#### Grafana
|
||||
|
||||
There is also a [nice Grafana dashboard](https://grafana.com/grafana/dashboards/12790), provided by the author of [wakatime_exporter](https://github.com/MacroPower/wakatime_exporter).
|
||||
|
||||

|
||||
|
||||
### WakaTime Integration
|
||||
Wakapi plays well together with [WakaTime](https://wakatime.com). For one thing, you can **forward heartbeats** from Wakapi to WakaTime to effectively use both services simultaneously. In addition, there is the option to **import historic data** from WakaTime for consistency between both services. Both features can be enabled in the _Integrations_ section of your Wakapi instance's settings page.
|
||||
### WakaTime integration
|
||||
|
||||
Wakapi plays well together with [WakaTime](https://wakatime.com). For one thing, you can **forward heartbeats** from Wakapi to WakaTime to effectively use both services simultaneously. In addition, there is the option to **import historic data** from WakaTime for consistency between both services. Both features can be enabled in the _Integrations_ section of your Wakapi instance's settings page.
|
||||
|
||||
### GitHub Readme Stats integrations
|
||||
|
||||
### GitHub Readme Stats Integrations
|
||||
Wakapi also integrates with [GitHub Readme Stats](https://github.com/anuraghazra/github-readme-stats#wakatime-week-stats) to generate fancy cards for you. Here is an example.
|
||||
|
||||

|
||||
@ -226,20 +263,20 @@ Wakapi also integrates with [GitHub Readme Stats](https://github.com/anuraghazra
|
||||
<details>
|
||||
<summary>Click to view code</summary>
|
||||
|
||||
```md
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
</details>
|
||||
<br>
|
||||
|
||||
### Github Readme Metrics integration
|
||||
|
||||
### Github Readme Metrics Integration
|
||||
There is a [WakaTime plugin](https://github.com/lowlighter/metrics/tree/master/source/plugins/wakatime) for GitHub [metrics](https://github.com/lowlighter/metrics/) that is also compatible with Wakapi.
|
||||
There is a [WakaTime plugin](https://github.com/lowlighter/metrics/tree/master/source/plugins/wakatime) for GitHub [Metrics](https://github.com/lowlighter/metrics/) that is also compatible with Wakapi.
|
||||
|
||||
Preview:
|
||||
|
||||

|
||||

|
||||
|
||||
<details>
|
||||
<summary>Click to view code</summary>
|
||||
@ -253,7 +290,7 @@ Preview:
|
||||
plugin_wakatime_days: 7 # Display last week stats
|
||||
plugin_wakatime_sections: time, projects, projects-graphs # Display time and projects sections, along with projects graphs
|
||||
plugin_wakatime_limit: 4 # Show 4 entries per graph
|
||||
plugin_wakatime_url: http://wakapi.dev # Wakatime url endpoint
|
||||
plugin_wakatime_url: http://wakapi.dev # Wakatime url endpoint
|
||||
plugin_wakatime_user: .user.login # User
|
||||
|
||||
```
|
||||
@ -261,20 +298,27 @@ Preview:
|
||||
</details>
|
||||
<br>
|
||||
|
||||
## 👍 Best Practices
|
||||
It is recommended to use wakapi behind a **reverse proxy**, like [Caddy](https://caddyserver.com) or _nginx_ to enable **TLS encryption** (HTTPS).
|
||||
However, if you want to expose your wakapi instance to the public anyway, you need to set `server.listen_ipv4` to `0.0.0.0` in `config.yml`
|
||||
## 👍 Best practices
|
||||
|
||||
It is recommended to use wakapi behind a **reverse proxy**, like [Caddy](https://caddyserver.com) or [nginx](https://www.nginx.com/), to enable **TLS encryption** (HTTPS).
|
||||
|
||||
However, if you want to expose your wakapi instance to the public anyway, you need to set `server.listen_ipv4` to `0.0.0.0` in `config.yml`.
|
||||
|
||||
## 🧪 Tests
|
||||
### Unit Tests
|
||||
|
||||
### Unit tests
|
||||
|
||||
|
||||
Unit tests are supposed to test business logic on a fine-grained level. They are implemented as part of the application, using Go's [testing](https://pkg.go.dev/testing?utm_source=godoc) package alongside [stretchr/testify](https://pkg.go.dev/github.com/stretchr/testify).
|
||||
|
||||
#### How to run
|
||||
|
||||
```bash
|
||||
$ CGO_FLAGS="-g -O2 -Wno-return-local-addr" go test -json -coverprofile=coverage/coverage.out ./... -run ./...
|
||||
$ CGO_ENABLED=0 go test `go list ./... | grep -v 'github.com/muety/wakapi/scripts'` -json -coverprofile=coverage/coverage.out ./... -run ./...
|
||||
```
|
||||
|
||||
### API Tests
|
||||
### API tests
|
||||
|
||||
API tests are implemented as black box tests, which interact with a fully-fledged, standalone Wakapi through HTTP requests. They are supposed to check Wakapi's web stack and endpoints, including response codes, headers and data on a syntactical level, rather than checking the actual content that is returned.
|
||||
|
||||
Our API (or end-to-end, in some way) tests are implemented as a [Postman](https://www.postman.com/) collection and can be run either from inside Postman, or using [newman](https://www.npmjs.com/package/newman) as a command-line runner.
|
||||
@ -282,6 +326,7 @@ Our API (or end-to-end, in some way) tests are implemented as a [Postman](https:
|
||||
To get a predictable environment, tests are run against a fresh and clean Wakapi instance with a SQLite database that is populated with nothing but some seed data (see [data.sql](testing/data.sql)). It is usually recommended for software tests to be [safe](https://www.restapitutorial.com/lessons/idempotency.html), stateless and without side effects. In contrary to that paradigm, our API tests strictly require a fixed execution order (which Postman assures) and their assertions may rely on specific previous tests having succeeded.
|
||||
|
||||
#### Prerequisites (Linux only)
|
||||
|
||||
```bash
|
||||
# 1. sqlite (cli)
|
||||
$ sudo apt install sqlite # Fedora: sudo dnf install sqlite
|
||||
@ -290,26 +335,31 @@ $ sudo apt install sqlite # Fedora: sudo dnf install sqlite
|
||||
$ npm install -g newman
|
||||
```
|
||||
|
||||
#### How to run (Linux only)
|
||||
#### How to run (Linux only)
|
||||
|
||||
```bash
|
||||
$ ./testing/run_api_tests.sh
|
||||
```
|
||||
|
||||
## 🤓 Developer Notes
|
||||
## 🤓 Developer notes
|
||||
|
||||
### Building web assets
|
||||
To keep things minimal, all JS and CSS assets are included as static files and checked in to Git. [TailwindCSS](https://tailwindcss.com/docs/installation#building-for-production) and [Iconify](https://iconify.design/docs/icon-bundles/) require an additional build step. To only require this at the time of development, the compiled assets are checked in to Git as well.
|
||||
|
||||
To keep things minimal, all JS and CSS assets are included as static files and checked in to Git. [TailwindCSS](https://tailwindcss.com/docs/installation#building-for-production) and [Iconify](https://iconify.design/docs/icon-bundles/) require an additional build step. To only require this at the time of development, the compiled assets are checked in to Git as well.
|
||||
|
||||
```bash
|
||||
$ yarn
|
||||
$ yarn build # or: yarn watch
|
||||
$ yarn build # or: yarn watch
|
||||
```
|
||||
|
||||
New icons can be added by editing the `icons` array in [scripts/bundle_icons.js](scripts/bundle_icons.js).
|
||||
|
||||
#### Precompression
|
||||
|
||||
As explained in [#284](https://github.com/muety/wakapi/issues/284), precompressed (using Brotli) versions of some of the assets are delivered to save additional bandwidth. This was inspired by Caddy's [`precompressed`](https://caddyserver.com/docs/caddyfile/directives/file_server) directive. [`gzipped.FileServer`](https://github.com/muety/wakapi/blob/07a367ce0a97c7738ba8e255e9c72df273fd43a3/main.go#L249) checks for every static file's `.br` or `.gz` equivalents and, if present, delivers those instead of the actual file, alongside `Content-Encoding: br`. Currently, compressed assets are simply checked in to Git. Later we might want to have this be part of a new build step.
|
||||
|
||||
To pre-compress files, run this:
|
||||
|
||||
```bash
|
||||
# Install brotli first
|
||||
$ sudo apt install brotli # or: sudo dnf install brotli
|
||||
@ -325,35 +375,36 @@ $ yarn compress
|
||||
```
|
||||
|
||||
## ❔ FAQs
|
||||
Since Wakapi heavily relies on the concepts provided by WakaTime, [their FAQs](https://wakatime.com/faq) apply to Wakapi for large parts as well. You might find answers there.
|
||||
|
||||
Since Wakapi heavily relies on the concepts provided by WakaTime, [their FAQs](https://wakatime.com/faq) largely apply to Wakapi as well. You might find answers there.
|
||||
|
||||
<details>
|
||||
<summary><b>What data is sent to Wakapi?</b></summary>
|
||||
<summary><b>What data are sent to Wakapi?</b></summary>
|
||||
|
||||
<ul>
|
||||
<li>File names</li>
|
||||
<li>Project names</li>
|
||||
<li>Editor names</li>
|
||||
<li>You computer's host name</li>
|
||||
<li>Your computer's host name</li>
|
||||
<li>Timestamps for every action you take in your editor</li>
|
||||
<li>...</li>
|
||||
</ul>
|
||||
|
||||
See the related [WakaTime FAQ section](https://wakatime.com/faq#data-collected) for details.
|
||||
|
||||
If you host Wakapi yourself, you have control over all your data. However, if you use our webservice and are concerned about privacy, you can also [exclude or obfuscate](https://wakatime.com/faq#exclude-paths) certain file- or project names.
|
||||
If you host Wakapi yourself, you have control over all your data. However, if you use our webservice and are concerned about privacy, you can also [exclude or obfuscate](https://wakatime.com/faq#exclude-paths) certain file- or project names.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>What happens if I'm offline?</b></summary>
|
||||
|
||||
All data is cached locally on your machine and sent in batches once you're online again.
|
||||
All data are cached locally on your machine and sent in batches once you're online again.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>How did Wakapi come about?</b></summary>
|
||||
|
||||
Wakapi was started when I was a student, who wanted to track detailed statistics about my coding time. Although I'm a big fan of WakaTime I didn't want to pay <a href="https://wakatime.com/pricing">$9 a month</a> back then. Luckily, most parts of WakaTime are open source!
|
||||
Wakapi was started when I was a student, who wanted to track detailed statistics about my coding time. Although I'm a big fan of WakaTime I didn't want to pay <a href="https://wakatime.com/pricing">$9 a month</a> back then. Luckily, most parts of WakaTime are open source!
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@ -366,11 +417,11 @@ Wakapi is a small subset of WakaTime and has a lot less features. Cool WakaTime
|
||||
<li><a href="https://wakatime.com/share/embed">Embeddable Charts</a></li>
|
||||
<li>Personal Goals</li>
|
||||
<li>Team- / Organization Support</li>
|
||||
<li>Integrations (with GitLab, etc.)</li>
|
||||
<li>Additional Integrations (with GitLab, etc.)</li>
|
||||
<li>Richer API</li>
|
||||
</ul>
|
||||
|
||||
WakaTime is worth the price. However, if you only want basic statistics and keep sovereignty over your data, you might want to go with Wakapi.
|
||||
WakaTime is worth the price. However, if you only need basic statistics and like to keep sovereignty over your data, you might want to go with Wakapi.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@ -380,13 +431,13 @@ Inferring a measure for your coding time from heartbeats works a bit differently
|
||||
|
||||
Here is an example (circles are heartbeats):
|
||||
|
||||
```
|
||||
```text
|
||||
|---o---o--------------o---o---|
|
||||
| |10s| 3m |10s| |
|
||||
|
||||
```
|
||||
|
||||
It is unclear how to handle the three minutes in between. Did the developer do a 3-minute break or were just no heartbeats being sent, e.g. because the developer was starring at the screen trying to find a solution, but not actually typing code.
|
||||
It is unclear how to handle the three minutes in between. Did the developer do a 3-minute break, or were just no heartbeats being sent, e.g. because the developer was staring at the screen trying to find a solution, but not actually typing code?
|
||||
|
||||
<ul>
|
||||
<li><b>WakaTime</b> (with 5 min timeout): 3 min 20 sec
|
||||
@ -398,17 +449,21 @@ Wakapi adds a "padding" of two minutes before the third heartbeat. This is why t
|
||||
</details>
|
||||
|
||||
## 🌳 Treeware
|
||||
|
||||
This package is [Treeware](https://treeware.earth). If you use it in production, then we ask that you [**buy the world a tree**](https://plant.treeware.earth/muety/wakapi) to thank us for our work. By contributing to the Treeware forest you’ll be creating employment for local families and restoring wildlife habitats.
|
||||
|
||||
## 👏 Support
|
||||
|
||||
Coding in open source is my passion and I would love to do it on a full-time basis and make a living from it one day. So if you like this project, please consider supporting it 🙂. You can donate either through [buying me a coffee](https://buymeacoff.ee/n1try) or becoming a GitHub sponsor. Every little donation is highly appreciated and boosts my motivation to keep improving Wakapi!
|
||||
|
||||
## 🙏 Thanks
|
||||
I highly appreciate the efforts of **[@alanhamlett](https://github.com/alanhamlett)** and the WakaTime team and am thankful for their software being open source.
|
||||
|
||||
I highly appreciate the efforts of **[@alanhamlett](https://github.com/alanhamlett)** and the WakaTime team and am thankful for their software being open source.
|
||||
|
||||
Moreover, thanks to **[JetBrains](https://jb.gg/OpenSource)** for supporting this project as part of their open-source program.
|
||||
|
||||

|
||||
|
||||
## 📓 License
|
||||
|
||||
GPL-v3 @ [Ferdinand Mütsch](https://muetsch.io)
|
||||
|
@ -1,9 +1,12 @@
|
||||
env: production
|
||||
quick_start: false # whether to skip initial tasks on application startup, like summary generation
|
||||
skip_migrations: false # whether to intentionally not run database migrations, only use for dev purposes
|
||||
|
||||
server:
|
||||
listen_ipv4: 127.0.0.1 # leave blank to disable ipv4
|
||||
listen_ipv6: ::1 # leave blank to disable ipv6
|
||||
listen_socket: # leave blank to disable unix sockets
|
||||
listen_socket_mode: 0666 # permission mode to create unix socket with
|
||||
timeout_sec: 30 # request timeout
|
||||
tls_cert_path: # leave blank to not use https
|
||||
tls_key_path: # leave blank to not use https
|
||||
@ -12,13 +15,20 @@ server:
|
||||
public_url: http://localhost:3000 # required for links (e.g. password reset) in e-mail
|
||||
|
||||
app:
|
||||
aggregation_time: '02:15' # time at which to run daily aggregation batch jobs
|
||||
report_time_weekly: 'fri,18:00' # time at which to fan out weekly reports (format: '<weekday)>,<daytime>')
|
||||
inactive_days: 7 # time of previous days within a user must have logged in to be considered active
|
||||
import_batch_size: 50 # maximum number of heartbeats to insert into the database within one transaction
|
||||
aggregation_time: '0 15 2 * * *' # time at which to run daily aggregation batch jobs
|
||||
leaderboard_generation_time: '0 0 6 * * *,0 0 18 * * *' # times at which to re-calculate the leaderboard
|
||||
report_time_weekly: '0 0 18 * * 5' # time at which to fan out weekly reports (extended cron)
|
||||
data_cleanup_time: '0 0 6 * * 0' # time at which to run old data cleanup (if enabled through data_retention_months)
|
||||
inactive_days: 7 # time of previous days within a user must have logged in to be considered active
|
||||
import_batch_size: 50 # maximum number of heartbeats to insert into the database within one transaction
|
||||
heartbeat_max_age: '4320h' # maximum acceptable age of a heartbeat (see https://pkg.go.dev/time#ParseDuration)
|
||||
data_retention_months: -1 # maximum retention period on months for user data (heartbeats) (-1 for infinity)
|
||||
custom_languages:
|
||||
vue: Vue
|
||||
jsx: JSX
|
||||
tsx: TSX
|
||||
cjs: JavaScript
|
||||
ipynb: Python
|
||||
svelte: Svelte
|
||||
|
||||
# url template for user avatar images (to be used with services like gravatar or dicebear)
|
||||
@ -29,6 +39,7 @@ app:
|
||||
db:
|
||||
host: # leave blank when using sqlite3
|
||||
port: # leave blank when using sqlite3
|
||||
socket: # alternative to db.host (leave blank when using sqlite3)
|
||||
user: # leave blank when using sqlite3
|
||||
password: # leave blank when using sqlite3
|
||||
name: wakapi_db.db # database name for mysql / postgres or file path for sqlite (e.g. /tmp/wakapi.db)
|
||||
@ -36,7 +47,7 @@ db:
|
||||
charset: utf8mb4 # only used for mysql connections
|
||||
max_conn: 2 # maximum number of concurrent connections to maintain
|
||||
ssl: false # whether to use tls for db connection (must be true for cockroachdb) (ignored for mysql and sqlite)
|
||||
automgirate_fail_silently: false # whether to ignore schema auto-migration failures when starting up
|
||||
automigrate_fail_silently: false # whether to ignore schema auto-migration failures when starting up
|
||||
|
||||
security:
|
||||
password_salt: # change this
|
||||
@ -52,6 +63,15 @@ sentry:
|
||||
sample_rate: 0.75 # probability of tracing a request
|
||||
sample_rate_heartbeats: 0.1 # probability of tracing a heartbeat request
|
||||
|
||||
# only relevant for running wakapi as a hosted service with paid subscriptions and stripe payments
|
||||
subscriptions:
|
||||
enabled: false
|
||||
expiry_notifications: true
|
||||
stripe_api_key:
|
||||
stripe_secret_key:
|
||||
stripe_endpoint_secret:
|
||||
standard_price_id:
|
||||
|
||||
mail:
|
||||
enabled: true # whether to enable mails (used for password resets, reports, etc.)
|
||||
provider: smtp # method for sending mails, currently one of ['smtp', 'mailwhale']
|
||||
@ -69,6 +89,4 @@ mail:
|
||||
mailwhale:
|
||||
url:
|
||||
client_id:
|
||||
client_secret:
|
||||
|
||||
quick_start: false # whether to skip initial tasks on application startup, like summary generation
|
||||
client_secret:
|
316
config/config.go
316
config/config.go
@ -4,10 +4,11 @@ import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/robfig/cron/v3"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -15,8 +16,8 @@ import (
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/jinzhu/configor"
|
||||
"github.com/muety/wakapi/data"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
"github.com/muety/wakapi/utils"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -26,9 +27,14 @@ const (
|
||||
SQLDialectPostgres = "postgres"
|
||||
SQLDialectSqlite = "sqlite3"
|
||||
|
||||
KeyLatestTotalTime = "latest_total_time"
|
||||
KeyLatestTotalUsers = "latest_total_users"
|
||||
KeyLastImportImport = "last_import"
|
||||
KeyLatestTotalTime = "latest_total_time"
|
||||
KeyLatestTotalUsers = "latest_total_users"
|
||||
KeyLastImportImport = "last_import"
|
||||
KeyFirstHeartbeat = "first_heartbeat"
|
||||
KeySubscriptionNotificationSent = "sub_reminder"
|
||||
KeyNewsbox = "newsbox"
|
||||
|
||||
SessionKeyDefault = "default"
|
||||
|
||||
SimpleDateFormat = "2006-01-02"
|
||||
SimpleDateTimeFormat = "2006-01-02 15:04:05"
|
||||
@ -63,15 +69,21 @@ var cFlag = flag.String("config", defaultConfigPath, "config file location")
|
||||
var env string
|
||||
|
||||
type appConfig struct {
|
||||
AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"`
|
||||
ReportTimeWeekly string `yaml:"report_time_weekly" default:"fri,18:00" env:"WAKAPI_REPORT_TIME_WEEKLY"`
|
||||
ImportBackoffMin int `yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN"`
|
||||
ImportBatchSize int `yaml:"import_batch_size" default:"50" env:"WAKAPI_IMPORT_BATCH_SIZE"`
|
||||
InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"`
|
||||
CountCacheTTLMin int `yaml:"count_cache_ttl_min" default:"30" env:"WAKAPI_COUNT_CACHE_TTL_MIN"`
|
||||
AvatarURLTemplate string `yaml:"avatar_url_template" default:"api/avatar/{username_hash}.svg"`
|
||||
CustomLanguages map[string]string `yaml:"custom_languages"`
|
||||
Colors map[string]map[string]string `yaml:"-"`
|
||||
AggregationTime string `yaml:"aggregation_time" default:"0 15 2 * * *" env:"WAKAPI_AGGREGATION_TIME"`
|
||||
LeaderboardGenerationTime string `yaml:"leaderboard_generation_time" default:"0 0 6 * * *,0 0 18 * * *" env:"WAKAPI_LEADERBOARD_GENERATION_TIME"`
|
||||
ReportTimeWeekly string `yaml:"report_time_weekly" default:"0 0 18 * * 5" env:"WAKAPI_REPORT_TIME_WEEKLY"`
|
||||
DataCleanupTime string `yaml:"data_cleanup_time" default:"0 0 6 * * 0" env:"WAKAPI_DATA_CLEANUP_TIME"`
|
||||
ImportBackoffMin int `yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN"`
|
||||
ImportBatchSize int `yaml:"import_batch_size" default:"50" env:"WAKAPI_IMPORT_BATCH_SIZE"`
|
||||
InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"`
|
||||
HeartbeatMaxAge string `yaml:"heartbeat_max_age" default:"4320h" env:"WAKAPI_HEARTBEAT_MAX_AGE"`
|
||||
CountCacheTTLMin int `yaml:"count_cache_ttl_min" default:"30" env:"WAKAPI_COUNT_CACHE_TTL_MIN"`
|
||||
DataRetentionMonths int `yaml:"data_retention_months" default:"-1" env:"WAKAPI_DATA_RETENTION_MONTHS"`
|
||||
DataCleanupDryRun bool `yaml:"data_cleanup_dry_run" default:"false" env:"WAKAPI_DATA_CLEANUP_DRY_RUN"` // for debugging only
|
||||
AvatarURLTemplate string `yaml:"avatar_url_template" default:"api/avatar/{username_hash}.svg" env:"WAKAPI_AVATAR_URL_TEMPLATE"`
|
||||
SupportContact string `yaml:"support_contact" default:"hostmaster@wakapi.dev" env:"WAKAPI_SUPPORT_CONTACT"`
|
||||
CustomLanguages map[string]string `yaml:"custom_languages"`
|
||||
Colors map[string]map[string]string `yaml:"-"`
|
||||
}
|
||||
|
||||
type securityConfig struct {
|
||||
@ -83,10 +95,12 @@ type securityConfig struct {
|
||||
InsecureCookies bool `yaml:"insecure_cookies" default:"false" env:"WAKAPI_INSECURE_COOKIES"`
|
||||
CookieMaxAgeSec int `yaml:"cookie_max_age" default:"172800" env:"WAKAPI_COOKIE_MAX_AGE"`
|
||||
SecureCookie *securecookie.SecureCookie `yaml:"-"`
|
||||
SessionKey []byte `yaml:"-"`
|
||||
}
|
||||
|
||||
type dbConfig struct {
|
||||
Host string `env:"WAKAPI_DB_HOST"`
|
||||
Socket string `env:"WAKAPI_DB_SOCKET"`
|
||||
Port uint `env:"WAKAPI_DB_PORT"`
|
||||
User string `env:"WAKAPI_DB_USER"`
|
||||
Password string `env:"WAKAPI_DB_PASSWORD"`
|
||||
@ -94,21 +108,33 @@ type dbConfig struct {
|
||||
Dialect string `yaml:"-"`
|
||||
Charset string `default:"utf8mb4" env:"WAKAPI_DB_CHARSET"`
|
||||
Type string `yaml:"dialect" default:"sqlite3" env:"WAKAPI_DB_TYPE"`
|
||||
DSN string `yaml:"DSN" default:"" env:"WAKAPI_DB_DSN"`
|
||||
MaxConn uint `yaml:"max_conn" default:"2" env:"WAKAPI_DB_MAX_CONNECTIONS"`
|
||||
Ssl bool `default:"false" env:"WAKAPI_DB_SSL"`
|
||||
AutoMigrateFailSilently bool `yaml:"automigrate_fail_silently" default:"false" env:"WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY"`
|
||||
}
|
||||
|
||||
type serverConfig struct {
|
||||
Port int `default:"3000" env:"WAKAPI_PORT"`
|
||||
ListenIpV4 string `yaml:"listen_ipv4" default:"127.0.0.1" env:"WAKAPI_LISTEN_IPV4"`
|
||||
ListenIpV6 string `yaml:"listen_ipv6" default:"::1" env:"WAKAPI_LISTEN_IPV6"`
|
||||
ListenSocket string `yaml:"listen_socket" default:"" env:"WAKAPI_LISTEN_SOCKET"`
|
||||
TimeoutSec int `yaml:"timeout_sec" default:"30" env:"WAKAPI_TIMEOUT_SEC"`
|
||||
BasePath string `yaml:"base_path" default:"/" env:"WAKAPI_BASE_PATH"`
|
||||
PublicUrl string `yaml:"public_url" default:"http://localhost:3000" env:"WAKAPI_PUBLIC_URL"`
|
||||
TlsCertPath string `yaml:"tls_cert_path" default:"" env:"WAKAPI_TLS_CERT_PATH"`
|
||||
TlsKeyPath string `yaml:"tls_key_path" default:"" env:"WAKAPI_TLS_KEY_PATH"`
|
||||
Port int `default:"3000" env:"WAKAPI_PORT"`
|
||||
ListenIpV4 string `yaml:"listen_ipv4" default:"127.0.0.1" env:"WAKAPI_LISTEN_IPV4"`
|
||||
ListenIpV6 string `yaml:"listen_ipv6" default:"::1" env:"WAKAPI_LISTEN_IPV6"`
|
||||
ListenSocket string `yaml:"listen_socket" default:"" env:"WAKAPI_LISTEN_SOCKET"`
|
||||
ListenSocketMode uint32 `yaml:"listen_socket_mode" default:"0666" env:"WAKAPI_LISTEN_SOCKET_MODE"`
|
||||
TimeoutSec int `yaml:"timeout_sec" default:"30" env:"WAKAPI_TIMEOUT_SEC"`
|
||||
BasePath string `yaml:"base_path" default:"/" env:"WAKAPI_BASE_PATH"`
|
||||
PublicUrl string `yaml:"public_url" default:"http://localhost:3000" env:"WAKAPI_PUBLIC_URL"`
|
||||
TlsCertPath string `yaml:"tls_cert_path" default:"" env:"WAKAPI_TLS_CERT_PATH"`
|
||||
TlsKeyPath string `yaml:"tls_key_path" default:"" env:"WAKAPI_TLS_KEY_PATH"`
|
||||
}
|
||||
|
||||
type subscriptionsConfig struct {
|
||||
Enabled bool `yaml:"enabled" default:"false" env:"WAKAPI_SUBSCRIPTIONS_ENABLED"`
|
||||
ExpiryNotifications bool `yaml:"expiry_notifications" default:"true" env:"WAKAPI_SUBSCRIPTIONS_EXPIRY_NOTIFICATIONS"`
|
||||
StripeApiKey string `yaml:"stripe_api_key" env:"WAKAPI_SUBSCRIPTIONS_STRIPE_API_KEY"`
|
||||
StripeSecretKey string `yaml:"stripe_secret_key" env:"WAKAPI_SUBSCRIPTIONS_STRIPE_SECRET_KEY"`
|
||||
StripeEndpointSecret string `yaml:"stripe_endpoint_secret" env:"WAKAPI_SUBSCRIPTIONS_STRIPE_ENDPOINT_SECRET"`
|
||||
StandardPriceId string `yaml:"standard_price_id" env:"WAKAPI_SUBSCRIPTIONS_STANDARD_PRICE_ID"`
|
||||
StandardPrice string `yaml:"-"`
|
||||
}
|
||||
|
||||
type sentryConfig struct {
|
||||
@ -141,16 +167,18 @@ type SMTPMailConfig struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Env string `default:"dev" env:"ENVIRONMENT"`
|
||||
Version string `yaml:"-"`
|
||||
QuickStart bool `yaml:"quick_start" env:"WAKAPI_QUICK_START"`
|
||||
InstanceId string `yaml:"-"` // only temporary, changes between runs
|
||||
App appConfig
|
||||
Security securityConfig
|
||||
Db dbConfig
|
||||
Server serverConfig
|
||||
Sentry sentryConfig
|
||||
Mail mailConfig
|
||||
Env string `default:"dev" env:"ENVIRONMENT"`
|
||||
Version string `yaml:"-"`
|
||||
QuickStart bool `yaml:"quick_start" env:"WAKAPI_QUICK_START"`
|
||||
SkipMigrations bool `yaml:"skip_migrations" env:"WAKAPI_SKIP_MIGRATIONS"`
|
||||
InstanceId string `yaml:"-"` // only temporary, changes between runs
|
||||
App appConfig
|
||||
Security securityConfig
|
||||
Db dbConfig
|
||||
Server serverConfig
|
||||
Subscriptions subscriptionsConfig
|
||||
Sentry sentryConfig
|
||||
Mail mailConfig
|
||||
}
|
||||
|
||||
func (c *Config) CreateCookie(name, value string) *http.Cookie {
|
||||
@ -169,7 +197,7 @@ func (c *Config) createCookie(name, value, path string, maxAge int) *http.Cookie
|
||||
MaxAge: maxAge,
|
||||
Secure: !c.Security.InsecureCookies,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,65 +209,102 @@ func (c *Config) UseTLS() bool {
|
||||
return c.Server.TlsCertPath != "" && c.Server.TlsKeyPath != ""
|
||||
}
|
||||
|
||||
func (c *Config) GetMigrationFunc(dbDialect string) models.MigrationFunc {
|
||||
switch dbDialect {
|
||||
default:
|
||||
return func(db *gorm.DB) error {
|
||||
if err := db.AutoMigrate(&models.User{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.KeyStringValue{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.Alias{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.Heartbeat{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.Summary{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.SummaryItem{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.LanguageMapping{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.ProjectLabel{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.Diagnostics{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *appConfig) GetCustomLanguages() map[string]string {
|
||||
return cloneStringMap(c.CustomLanguages, false)
|
||||
return utils.CloneStringMap(c.CustomLanguages, false)
|
||||
}
|
||||
|
||||
func (c *appConfig) GetLanguageColors() map[string]string {
|
||||
return cloneStringMap(c.Colors["languages"], true)
|
||||
return utils.CloneStringMap(c.Colors["languages"], true)
|
||||
}
|
||||
|
||||
func (c *appConfig) GetEditorColors() map[string]string {
|
||||
return cloneStringMap(c.Colors["editors"], true)
|
||||
return utils.CloneStringMap(c.Colors["editors"], true)
|
||||
}
|
||||
|
||||
func (c *appConfig) GetOSColors() map[string]string {
|
||||
return cloneStringMap(c.Colors["operating_systems"], true)
|
||||
return utils.CloneStringMap(c.Colors["operating_systems"], true)
|
||||
}
|
||||
|
||||
func (c *appConfig) GetWeeklyReportDay() time.Weekday {
|
||||
s := strings.Split(c.ReportTimeWeekly, ",")[0]
|
||||
return parseWeekday(s)
|
||||
func (c *appConfig) GetAggregationTimeCron() string {
|
||||
if strings.Contains(c.AggregationTime, ":") {
|
||||
// old gocron format, e.g. "15:04"
|
||||
timeParts := strings.Split(c.AggregationTime, ":")
|
||||
h, err := strconv.Atoi(timeParts[0])
|
||||
if err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
}
|
||||
|
||||
m, err := strconv.Atoi(timeParts[1])
|
||||
if err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
}
|
||||
|
||||
return fmt.Sprintf("0 %d %d * * *", m, h)
|
||||
}
|
||||
|
||||
return utils.CronPadToSecondly(c.AggregationTime)
|
||||
}
|
||||
|
||||
func (c *appConfig) GetWeeklyReportTime() string {
|
||||
return strings.Split(c.ReportTimeWeekly, ",")[1]
|
||||
func (c *appConfig) GetWeeklyReportCron() string {
|
||||
if strings.Contains(c.ReportTimeWeekly, ",") {
|
||||
// old gocron format, e.g. "fri,18:00"
|
||||
split := strings.Split(c.ReportTimeWeekly, ",")
|
||||
weekday := utils.ParseWeekday(split[0])
|
||||
timeParts := strings.Split(split[1], ":")
|
||||
|
||||
h, err := strconv.Atoi(timeParts[0])
|
||||
if err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
}
|
||||
|
||||
m, err := strconv.Atoi(timeParts[1])
|
||||
if err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
}
|
||||
|
||||
return fmt.Sprintf("0 %d %d * * %d", m, h, weekday)
|
||||
}
|
||||
|
||||
return utils.CronPadToSecondly(c.ReportTimeWeekly)
|
||||
}
|
||||
|
||||
func (c *appConfig) GetLeaderboardGenerationTimeCron() []string {
|
||||
crons := []string{}
|
||||
|
||||
var parse func(string) string
|
||||
|
||||
if strings.Contains(c.LeaderboardGenerationTime, ":") {
|
||||
// old gocron format, e.g. "15:04"
|
||||
parse = func(s string) string {
|
||||
timeParts := strings.Split(s, ":")
|
||||
h, err := strconv.Atoi(timeParts[0])
|
||||
if err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
}
|
||||
|
||||
m, err := strconv.Atoi(timeParts[1])
|
||||
if err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
}
|
||||
|
||||
return fmt.Sprintf("0 %d %d * * *", m, h)
|
||||
}
|
||||
} else {
|
||||
parse = func(s string) string {
|
||||
return utils.CronPadToSecondly(s)
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range utils.SplitMulti(c.LeaderboardGenerationTime, ",", ";") {
|
||||
crons = append(crons, parse(strings.TrimSpace(s)))
|
||||
}
|
||||
|
||||
return crons
|
||||
}
|
||||
|
||||
func (c *appConfig) HeartbeatsMaxAge() time.Duration {
|
||||
d, _ := time.ParseDuration(c.HeartbeatMaxAge)
|
||||
return d
|
||||
}
|
||||
|
||||
func (c *dbConfig) IsSQLite() bool {
|
||||
@ -289,13 +354,6 @@ func readColors() map[string]map[string]string {
|
||||
return colors
|
||||
}
|
||||
|
||||
func mustReadConfigLocation() string {
|
||||
if _, err := os.Stat(*cFlag); err != nil {
|
||||
logbuch.Fatal("failed to find config file at '%s'", *cFlag)
|
||||
}
|
||||
return *cFlag
|
||||
}
|
||||
|
||||
func resolveDbDialect(dbType string) string {
|
||||
if dbType == "cockroach" {
|
||||
return "postgres"
|
||||
@ -309,35 +367,6 @@ func resolveDbDialect(dbType string) string {
|
||||
return dbType
|
||||
}
|
||||
|
||||
func findString(needle string, haystack []string, defaultVal string) string {
|
||||
for _, s := range haystack {
|
||||
if s == needle {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
func parseWeekday(s string) time.Weekday {
|
||||
switch strings.ToLower(s) {
|
||||
case "mon", strings.ToLower(time.Monday.String()):
|
||||
return time.Monday
|
||||
case "tue", strings.ToLower(time.Tuesday.String()):
|
||||
return time.Tuesday
|
||||
case "wed", strings.ToLower(time.Wednesday.String()):
|
||||
return time.Wednesday
|
||||
case "thu", strings.ToLower(time.Thursday.String()):
|
||||
return time.Thursday
|
||||
case "fri", strings.ToLower(time.Friday.String()):
|
||||
return time.Friday
|
||||
case "sat", strings.ToLower(time.Saturday.String()):
|
||||
return time.Saturday
|
||||
case "sun", strings.ToLower(time.Sunday.String()):
|
||||
return time.Sunday
|
||||
}
|
||||
return time.Monday
|
||||
}
|
||||
|
||||
func Set(config *Config) {
|
||||
cfg = config
|
||||
}
|
||||
@ -351,12 +380,18 @@ func Load(version string) *Config {
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if err := configor.New(&configor.Config{}).Load(config, mustReadConfigLocation()); err != nil {
|
||||
if err := configor.New(&configor.Config{}).Load(config, *cFlag); err != nil {
|
||||
logbuch.Fatal("failed to read config: %v", err)
|
||||
}
|
||||
|
||||
env = config.Env
|
||||
|
||||
config.Version = strings.TrimSpace(version)
|
||||
tagVersionMatch, _ := regexp.MatchString(`\d+\.\d+\.\d+`, version)
|
||||
if tagVersionMatch {
|
||||
config.Version = "v" + config.Version
|
||||
}
|
||||
|
||||
config.InstanceId = uuid.NewV4().String()
|
||||
config.App.Colors = readColors()
|
||||
config.Db.Dialect = resolveDbDialect(config.Db.Type)
|
||||
@ -364,6 +399,7 @@ func Load(version string) *Config {
|
||||
securecookie.GenerateRandomKey(64),
|
||||
securecookie.GenerateRandomKey(32),
|
||||
)
|
||||
config.Security.SessionKey = securecookie.GenerateRandomKey(32)
|
||||
|
||||
if strings.HasSuffix(config.Server.BasePath, "/") {
|
||||
config.Server.BasePath = config.Server.BasePath[:len(config.Server.BasePath)-1]
|
||||
@ -380,8 +416,18 @@ func Load(version string) *Config {
|
||||
initSentry(config.Sentry, config.IsDev())
|
||||
}
|
||||
|
||||
if config.App.DataRetentionMonths <= 0 {
|
||||
logbuch.Info("disabling data retention policy, keeping data forever")
|
||||
} else {
|
||||
dataRetentionWarning := fmt.Sprintf("⚠️ data retention policy will cause user data older than %d months to be deleted", config.App.DataRetentionMonths)
|
||||
if config.Subscriptions.Enabled {
|
||||
dataRetentionWarning += " (except for users with active subscriptions)"
|
||||
}
|
||||
logbuch.Warn(dataRetentionWarning)
|
||||
}
|
||||
|
||||
// some validation checks
|
||||
if config.Server.ListenIpV4 == "" && config.Server.ListenIpV6 == "" && config.Server.ListenSocket == "" {
|
||||
if config.Server.ListenIpV4 == "-" && config.Server.ListenIpV6 == "-" && config.Server.ListenSocket == "" {
|
||||
logbuch.Fatal("either of listen_ipv4 or listen_ipv6 or listen_socket must be set")
|
||||
}
|
||||
if config.Db.MaxConn <= 0 {
|
||||
@ -391,14 +437,36 @@ func Load(version string) *Config {
|
||||
logbuch.Warn("with sqlite, only a single connection is supported") // otherwise 'PRAGMA foreign_keys=ON' would somehow have to be set for every connection in the pool
|
||||
config.Db.MaxConn = 1
|
||||
}
|
||||
if config.Mail.Provider != "" && findString(config.Mail.Provider, emailProviders, "") == "" {
|
||||
if config.Mail.Provider != "" && utils.FindString(config.Mail.Provider, emailProviders, "") == "" {
|
||||
logbuch.Fatal("unknown mail provider '%s'", config.Mail.Provider)
|
||||
}
|
||||
if _, err := time.Parse("15:04", config.App.GetWeeklyReportTime()); err != nil {
|
||||
logbuch.Fatal("invalid interval set for report_time_weekly")
|
||||
if _, err := time.ParseDuration(config.App.HeartbeatMaxAge); err != nil {
|
||||
logbuch.Fatal("invalid duration set for heartbeat_max_age")
|
||||
}
|
||||
if _, err := time.Parse("15:04", config.App.AggregationTime); err != nil {
|
||||
logbuch.Fatal("invalid interval set for aggregation_time")
|
||||
|
||||
cronParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||
|
||||
if _, err := cronParser.Parse(config.App.GetWeeklyReportCron()); err != nil {
|
||||
logbuch.Fatal("invalid cron expression for report_time_weekly")
|
||||
}
|
||||
if _, err := cronParser.Parse(config.App.GetAggregationTimeCron()); err != nil {
|
||||
logbuch.Fatal("invalid cron expression for aggregation_time")
|
||||
}
|
||||
for _, c := range config.App.GetLeaderboardGenerationTimeCron() {
|
||||
if _, err := cronParser.Parse(c); err != nil {
|
||||
logbuch.Fatal("invalid cron expression for leaderboard_generation_time")
|
||||
}
|
||||
}
|
||||
|
||||
// deprecation notices
|
||||
if strings.Contains(config.App.AggregationTime, ":") {
|
||||
logbuch.Warn("you're using deprecated syntax for 'aggregation_time', please change it to a valid cron expression")
|
||||
}
|
||||
if strings.Contains(config.App.ReportTimeWeekly, ":") {
|
||||
logbuch.Warn("you're using deprecated syntax for 'report_time_weekly', please change it to a valid cron expression")
|
||||
}
|
||||
if strings.Contains(config.App.LeaderboardGenerationTime, ":") {
|
||||
logbuch.Warn("you're using deprecated syntax for 'leaderboard_generation_time', please change it to a semicolon-separated list if valid cron expressions")
|
||||
}
|
||||
|
||||
Set(config)
|
||||
|
@ -2,8 +2,9 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfig_IsDev(t *testing.T) {
|
||||
@ -37,6 +38,28 @@ func Test_mysqlConnectionString(t *testing.T) {
|
||||
), mysqlConnectionString(c))
|
||||
}
|
||||
|
||||
func Test_mysqlConnectionStringSocket(t *testing.T) {
|
||||
c := &dbConfig{
|
||||
Socket: "/var/run/mysql.sock",
|
||||
Port: 9999,
|
||||
User: "test_user",
|
||||
Password: "test_password",
|
||||
Name: "test_name",
|
||||
Dialect: "mysql",
|
||||
Charset: "utf8mb4",
|
||||
MaxConn: 10,
|
||||
}
|
||||
|
||||
assert.Equal(t, fmt.Sprintf(
|
||||
"%s:%s@unix(%s)/%s?charset=utf8mb4&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
|
||||
c.User,
|
||||
c.Password,
|
||||
c.Socket,
|
||||
c.Name,
|
||||
"Local",
|
||||
), mysqlConnectionString(c))
|
||||
}
|
||||
|
||||
func Test_postgresConnectionString(t *testing.T) {
|
||||
c := &dbConfig{
|
||||
Host: "test_host",
|
||||
|
18
config/db.go
18
config/db.go
@ -2,9 +2,10 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@ -54,11 +55,16 @@ func (c *dbConfig) GetDialector() gorm.Dialector {
|
||||
}
|
||||
|
||||
func mysqlConnectionString(config *dbConfig) string {
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
|
||||
host := fmt.Sprintf("tcp(%s:%d)", config.Host, config.Port)
|
||||
|
||||
if config.Socket != "" {
|
||||
host = fmt.Sprintf("unix(%s)", config.Socket)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%s@%s/%s?charset=%s&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
|
||||
config.User,
|
||||
config.Password,
|
||||
config.Host,
|
||||
config.Port,
|
||||
host,
|
||||
config.Name,
|
||||
config.Charset,
|
||||
"Local",
|
||||
@ -66,6 +72,10 @@ func mysqlConnectionString(config *dbConfig) string {
|
||||
}
|
||||
|
||||
func postgresConnectionString(config *dbConfig) string {
|
||||
if len(config.DSN) > 0 {
|
||||
return config.DSN
|
||||
}
|
||||
|
||||
sslmode := "disable"
|
||||
if config.Ssl {
|
||||
sslmode = "require"
|
||||
|
35
config/db_opts.go
Normal file
35
config/db_opts.go
Normal file
@ -0,0 +1,35 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type WakapiDBOpts struct {
|
||||
dbConfig *dbConfig
|
||||
}
|
||||
|
||||
func GetWakapiDBOpts(dbConfig *dbConfig) *WakapiDBOpts {
|
||||
return &WakapiDBOpts{dbConfig: dbConfig}
|
||||
}
|
||||
|
||||
func (opts WakapiDBOpts) Apply(config *gorm.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (opts WakapiDBOpts) AfterInitialize(db *gorm.DB) error {
|
||||
// initial session variables
|
||||
if opts.dbConfig.Type == "cockroach" {
|
||||
// https://www.cockroachlabs.com/docs/stable/experimental-features.html#alter-column-types
|
||||
if err := db.Exec("SET enable_experimental_alter_column_type_general = true;").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.dbConfig.IsSQLite() {
|
||||
if err := db.Exec("PRAGMA foreign_keys = ON;").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
85
config/jobqueue.go
Normal file
85
config/jobqueue.go
Normal file
@ -0,0 +1,85 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/muety/artifex/v2"
|
||||
"math"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var jobQueues map[string]*artifex.Dispatcher
|
||||
var jobCounts map[string]int
|
||||
|
||||
const (
|
||||
QueueDefault = "wakapi.default"
|
||||
QueueProcessing = "wakapi.processing"
|
||||
QueueReports = "wakapi.reports"
|
||||
QueueMails = "wakapi.mail"
|
||||
QueueImports = "wakapi.imports"
|
||||
QueueHousekeeping = "wakapi.housekeeping"
|
||||
)
|
||||
|
||||
type JobQueueMetrics struct {
|
||||
Queue string
|
||||
EnqueuedJobs int
|
||||
FinishedJobs int
|
||||
}
|
||||
|
||||
func init() {
|
||||
jobQueues = make(map[string]*artifex.Dispatcher)
|
||||
|
||||
InitQueue(QueueDefault, 1)
|
||||
InitQueue(QueueProcessing, halfCPUs())
|
||||
InitQueue(QueueReports, 1)
|
||||
InitQueue(QueueMails, 1)
|
||||
InitQueue(QueueImports, 1)
|
||||
InitQueue(QueueHousekeeping, halfCPUs())
|
||||
}
|
||||
|
||||
func InitQueue(name string, workers int) error {
|
||||
if _, ok := jobQueues[name]; ok {
|
||||
return fmt.Errorf("queue '%s' already existing", name)
|
||||
}
|
||||
logbuch.Info("creating job queue '%s' (%d workers)", name, workers)
|
||||
jobQueues[name] = artifex.NewDispatcher(workers, 4096)
|
||||
jobQueues[name].Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDefaultQueue() *artifex.Dispatcher {
|
||||
return GetQueue(QueueDefault)
|
||||
}
|
||||
|
||||
func GetQueue(name string) *artifex.Dispatcher {
|
||||
if _, ok := jobQueues[name]; !ok {
|
||||
InitQueue(name, 1)
|
||||
}
|
||||
return jobQueues[name]
|
||||
}
|
||||
|
||||
func GetQueueMetrics() []*JobQueueMetrics {
|
||||
metrics := make([]*JobQueueMetrics, 0, len(jobQueues))
|
||||
for name, queue := range jobQueues {
|
||||
metrics = append(metrics, &JobQueueMetrics{
|
||||
Queue: name,
|
||||
EnqueuedJobs: queue.CountEnqueued(),
|
||||
FinishedJobs: queue.CountDispatched(),
|
||||
})
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
|
||||
func CloseQueues() {
|
||||
for _, q := range jobQueues {
|
||||
q.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func allCPUs() int {
|
||||
return runtime.NumCPU()
|
||||
}
|
||||
|
||||
func halfCPUs() int {
|
||||
return int(math.Ceil(float64(runtime.NumCPU()) / 2.0))
|
||||
}
|
@ -3,7 +3,6 @@ package config
|
||||
import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/muety/wakapi/models"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -89,8 +88,8 @@ func (l *SentryWrapperLogger) log(msg string, level sentry.Level) {
|
||||
if h := l.req.Context().Value(sentry.HubContextKey); h != nil {
|
||||
hub := h.(*sentry.Hub)
|
||||
hub.Scope().SetRequest(l.req)
|
||||
if u := getPrincipal(l.req); u != nil {
|
||||
hub.Scope().SetUser(sentry.User{ID: u.ID})
|
||||
if uid := getPrincipal(l.req); uid != "" {
|
||||
hub.Scope().SetUser(sentry.User{ID: uid})
|
||||
}
|
||||
hub.CaptureEvent(event)
|
||||
return
|
||||
@ -109,8 +108,9 @@ var excludedRoutes = []string{
|
||||
|
||||
func initSentry(config sentryConfig, debug bool) {
|
||||
if err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: config.Dsn,
|
||||
Debug: debug,
|
||||
Dsn: config.Dsn,
|
||||
Debug: debug,
|
||||
AttachStacktrace: true,
|
||||
TracesSampler: sentry.TracesSamplerFunc(func(ctx sentry.SamplingContext) sentry.Sampled {
|
||||
if !config.EnableTracing {
|
||||
return sentry.SampledFalse
|
||||
@ -132,8 +132,8 @@ func initSentry(config sentryConfig, debug bool) {
|
||||
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
|
||||
if hint.Context != nil {
|
||||
if req, ok := hint.Context.Value(sentry.RequestContextKey).(*http.Request); ok {
|
||||
if u := getPrincipal(req); u != nil {
|
||||
event.User.ID = u.ID
|
||||
if uid := getPrincipal(req); uid != "" {
|
||||
event.User.ID = uid
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -144,12 +144,14 @@ func initSentry(config sentryConfig, debug bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func getPrincipal(r *http.Request) *models.User {
|
||||
type principalGetter interface {
|
||||
GetPrincipal() *models.User
|
||||
// returns a user id
|
||||
func getPrincipal(r *http.Request) string {
|
||||
type principalIdentityGetter interface {
|
||||
GetPrincipalIdentity() string
|
||||
}
|
||||
|
||||
if p := r.Context().Value("principal"); p != nil {
|
||||
return p.(principalGetter).GetPrincipal()
|
||||
return p.(principalIdentityGetter).GetPrincipalIdentity()
|
||||
}
|
||||
return nil
|
||||
return ""
|
||||
}
|
||||
|
14
config/session.go
Normal file
14
config/session.go
Normal file
@ -0,0 +1,14 @@
|
||||
package config
|
||||
|
||||
import "github.com/gorilla/sessions"
|
||||
|
||||
// sessions are only used for displaying flash messages
|
||||
|
||||
var sessionStore *sessions.CookieStore
|
||||
|
||||
func GetSessionStore() *sessions.CookieStore {
|
||||
if sessionStore == nil {
|
||||
sessionStore = sessions.NewCookieStore(Get().Security.SessionKey)
|
||||
}
|
||||
return sessionStore
|
||||
}
|
@ -9,4 +9,5 @@ const (
|
||||
ResetPasswordTemplate = "reset-password.tpl.html"
|
||||
SettingsTemplate = "settings.tpl.html"
|
||||
SummaryTemplate = "summary.tpl.html"
|
||||
LeaderboardTemplate = "leaderboard.tpl.html"
|
||||
)
|
||||
|
@ -1,14 +0,0 @@
|
||||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
func cloneStringMap(m map[string]string, keysToLower bool) map[string]string {
|
||||
m2 := make(map[string]string)
|
||||
for k, v := range m {
|
||||
if keysToLower {
|
||||
k = strings.ToLower(k)
|
||||
}
|
||||
m2[k] = v
|
||||
}
|
||||
return m2
|
||||
}
|
File diff suppressed because it is too large
Load Diff
391
data/colors.json
391
data/colors.json
@ -2,246 +2,417 @@
|
||||
"languages": {
|
||||
"1C Enterprise": "#814CCC",
|
||||
"ABAP": "#E8274B",
|
||||
"ActionScript": "#882B0F",
|
||||
"Ada": "#02f88c",
|
||||
"Agda": "#315665",
|
||||
"AGS Script": "#B9D9FF",
|
||||
"Alloy": "#64C800",
|
||||
"AL": "#3AA2B5",
|
||||
"AMPL": "#E6EFBB",
|
||||
"AngelScript": "#C7D7DC",
|
||||
"ANTLR": "#9DC3FF",
|
||||
"API Blueprint": "#2ACCA8",
|
||||
"APL": "#5A8164",
|
||||
"AppleScript": "#101F1F",
|
||||
"Arc": "#aa2afe",
|
||||
"ASP": "#6a40fd",
|
||||
"AspectJ": "#a957b0",
|
||||
"Assembly": "#6E4C13",
|
||||
"Asymptote": "#4a0c0c",
|
||||
"APL": "#8a0707",
|
||||
"ASP.NET": "#9400ff",
|
||||
"ATS": "#1ac620",
|
||||
"ActionScript": "#e3491a",
|
||||
"Ada": "#02f88c",
|
||||
"Agda": "#467C91",
|
||||
"Alloy": "#cc5c24",
|
||||
"AngelScript": "#C7D7DC",
|
||||
"Apex": "#1797c0",
|
||||
"Apollo Guidance Computer": "#0B3D91",
|
||||
"AppleScript": "#101F1F",
|
||||
"Arc": "#ca2afe",
|
||||
"Arduino": "#bd79d1",
|
||||
"AspectJ": "#1957b0",
|
||||
"Assembly": "#6E4C13",
|
||||
"Asymptote": "#ff0000",
|
||||
"Augeas": "#62331f",
|
||||
"AutoHotkey": "#6594b9",
|
||||
"AutoIt": "#1C3552",
|
||||
"AutoIt": "#36699B",
|
||||
"Ballerina": "#FF5000",
|
||||
"Batchfile": "#C1F12E",
|
||||
"Beef": "#a52f4e",
|
||||
"Bison": "#6A463F",
|
||||
"Blade": "#f7523f",
|
||||
"BlitzMax": "#cd6400",
|
||||
"Boo": "#d4bec1",
|
||||
"Boogie": "#c80fa0",
|
||||
"Brainfuck": "#2F2530",
|
||||
"Browserslist": "#ffd539",
|
||||
"C": "#555555",
|
||||
"C#": "#178600",
|
||||
"C Sharp": "#178600",
|
||||
"C#": "#5a25a2",
|
||||
"C++": "#f34b7d",
|
||||
"CSON": "#244776",
|
||||
"CSS": "#563d7c",
|
||||
"Ceylon": "#dfa535",
|
||||
"Chapel": "#8dc63f",
|
||||
"Cirru": "#ccccff",
|
||||
"Cirru": "#aaaaff",
|
||||
"Clarion": "#db901e",
|
||||
"Clean": "#3F85AF",
|
||||
"Classic ASP": "#6a40fd",
|
||||
"Clean": "#3a81ad",
|
||||
"Click": "#E4E6F3",
|
||||
"Clojure": "#db5855",
|
||||
"Closure Templates": "#0d948f",
|
||||
"CoffeeScript": "#244776",
|
||||
"ColdFusion": "#ed2cd6",
|
||||
"ColdFusion CFC": "#ed2cd6",
|
||||
"Common Lisp": "#3fb68b",
|
||||
"Common Workflow Language": "#B5314C",
|
||||
"Component Pascal": "#B0CE4E",
|
||||
"Component Pascal": "#b0ce4e",
|
||||
"Crystal": "#000100",
|
||||
"CSS": "#563d7c",
|
||||
"Cuda": "#3A4E3A",
|
||||
"D": "#ba595e",
|
||||
"Dart": "#00B4AB",
|
||||
"D": "#fcd46d",
|
||||
"DM": "#075ff1",
|
||||
"Dafny": "#FFEC25",
|
||||
"Dart": "#98BAD6",
|
||||
"DataWeave": "#003a52",
|
||||
"DM": "#447265",
|
||||
"Denizen": "#faf094",
|
||||
"Dhall": "#dfafff",
|
||||
"Dockerfile": "#384d54",
|
||||
"Docker": "#384d54",
|
||||
"Dogescript": "#cca760",
|
||||
"Dylan": "#6c616e",
|
||||
"Dylan": "#3ebc27",
|
||||
"E": "#ccce35",
|
||||
"eC": "#913960",
|
||||
"ECL": "#8a1267",
|
||||
"EJS": "#a91e50",
|
||||
"EQ": "#a78649",
|
||||
"Eagle": "#3994bc",
|
||||
"Eiffel": "#946d57",
|
||||
"Elixir": "#6e4a7e",
|
||||
"Elm": "#60B5CC",
|
||||
"Emacs Lisp": "#c065db",
|
||||
"EmberScript": "#FFF4F3",
|
||||
"EQ": "#a78649",
|
||||
"Erlang": "#B83998",
|
||||
"EmberScript": "#f64e3e",
|
||||
"Erlang": "#0faf8d",
|
||||
"F#": "#b845fc",
|
||||
"F*": "#572e30",
|
||||
"FLUX": "#33CCFF",
|
||||
"FORTRAN": "#4d41b1",
|
||||
"Factor": "#636746",
|
||||
"Fancy": "#7b9db4",
|
||||
"Fantom": "#14253c",
|
||||
"FLUX": "#88ccff",
|
||||
"Fantom": "#dbded5",
|
||||
"Faust": "#c37240",
|
||||
"Forth": "#341708",
|
||||
"Fortran": "#4d41b1",
|
||||
"FreeMarker": "#0050b2",
|
||||
"Frege": "#00cafe",
|
||||
"Game Maker Language": "#71b417",
|
||||
"Futhark": "#5f021f",
|
||||
"G-code": "#D08CF2",
|
||||
"GAML": "#FFC766",
|
||||
"GDScript": "#355570",
|
||||
"Game Maker Language": "#8ad353",
|
||||
"Genie": "#fb855d",
|
||||
"Gherkin": "#5B2063",
|
||||
"Glyph": "#c1ac7f",
|
||||
"Glyph": "#e4cc98",
|
||||
"Gnuplot": "#f0a9f0",
|
||||
"Go": "#00ADD8",
|
||||
"Golo": "#88562A",
|
||||
"Go": "#375eab",
|
||||
"Golo": "#f6a51f",
|
||||
"Gosu": "#82937f",
|
||||
"Grammatical Framework": "#79aa7a",
|
||||
"Grammatical Framework": "#ff0000",
|
||||
"GraphQL": "#e10098",
|
||||
"Groovy": "#e69f56",
|
||||
"HTML": "#e44b23",
|
||||
"Hack": "#878787",
|
||||
"Haml": "#ece2a9",
|
||||
"Handlebars": "#f7931e",
|
||||
"Harbour": "#0e60e3",
|
||||
"Haskell": "#5e5086",
|
||||
"Haxe": "#df7900",
|
||||
"Haskell": "#29b544",
|
||||
"Haxe": "#f7941e",
|
||||
"HiveQL": "#dce200",
|
||||
"HTML": "#e34c26",
|
||||
"Hy": "#7790B2",
|
||||
"IDL": "#a3522f",
|
||||
"HolyC": "#ffefaf",
|
||||
"Hy": "#7891b1",
|
||||
"IDL": "#e3592c",
|
||||
"IGOR Pro": "#0000cc",
|
||||
"Idris": "#b30000",
|
||||
"ImageJ Macro": "#99AAFF",
|
||||
"Io": "#a9188d",
|
||||
"Ioke": "#078193",
|
||||
"Isabelle": "#FEFE00",
|
||||
"Isabelle": "#fdcd00",
|
||||
"J": "#9EEDFF",
|
||||
"JFlex": "#DBCA00",
|
||||
"JSONiq": "#40d47e",
|
||||
"Java": "#b07219",
|
||||
"JavaScript": "#f1e05a",
|
||||
"Jolie": "#843179",
|
||||
"JSONiq": "#40d47e",
|
||||
"Jsonnet": "#0064bd",
|
||||
"Julia": "#a270ba",
|
||||
"Jupyter Notebook": "#DA5B0B",
|
||||
"KRL": "#f5c800",
|
||||
"Kaitai Struct": "#773b37",
|
||||
"Kotlin": "#F18E33",
|
||||
"KRL": "#28430A",
|
||||
"Lasso": "#999999",
|
||||
"Lex": "#DBCA00",
|
||||
"LFE": "#4C3023",
|
||||
"LiveScript": "#499886",
|
||||
"LFE": "#004200",
|
||||
"LLVM": "#185619",
|
||||
"LOLCODE": "#cc9900",
|
||||
"LookML": "#652B81",
|
||||
"LSL": "#3d9970",
|
||||
"Lua": "#000080",
|
||||
"Makefile": "#427819",
|
||||
"Mask": "#f97732",
|
||||
"Lark": "#0b130f",
|
||||
"Lasso": "#2584c3",
|
||||
"Latte": "#A8FF97",
|
||||
"Less": "#1d365d",
|
||||
"Lex": "#DBCA00",
|
||||
"Liquid": "#67b8de",
|
||||
"LiveScript": "#499886",
|
||||
"LookML": "#652B81",
|
||||
"Lua": "#fa1fa1",
|
||||
"MATLAB": "#e16737",
|
||||
"Max": "#c4a79c",
|
||||
"MAXScript": "#00a6a6",
|
||||
"mcfunction": "#E22837",
|
||||
"Mercury": "#ff2b2b",
|
||||
"MLIR": "#5EC8DB",
|
||||
"MQL4": "#62A8D6",
|
||||
"MQL5": "#4A76B8",
|
||||
"MTML": "#0095d9",
|
||||
"Macaulay2": "#d8ffff",
|
||||
"Makefile": "#427819",
|
||||
"Markdown": "#083fa1",
|
||||
"Marko": "#42bff2",
|
||||
"Mask": "#f97732",
|
||||
"Matlab": "#bb92ac",
|
||||
"Max": "#ce279c",
|
||||
"Mercury": "#abcdef",
|
||||
"Meson": "#007800",
|
||||
"Metal": "#8f14e9",
|
||||
"Mirah": "#c7a938",
|
||||
"Modula-3": "#223388",
|
||||
"MQL4": "#62A8D6",
|
||||
"MQL5": "#4A76B8",
|
||||
"MTML": "#b7e1f4",
|
||||
"Mustache": "#724b3b",
|
||||
"NCL": "#28431f",
|
||||
"NWScript": "#111522",
|
||||
"Nearley": "#990000",
|
||||
"Nemerle": "#3d3c6e",
|
||||
"nesC": "#94B0C7",
|
||||
"Nemerle": "#0d3c6e",
|
||||
"NetLinx": "#0aa0ff",
|
||||
"NetLinx+ERB": "#747faa",
|
||||
"NetLogo": "#ff6375",
|
||||
"NewLisp": "#87AED7",
|
||||
"NetLogo": "#ff2b2b",
|
||||
"NewLisp": "#eedd66",
|
||||
"Nextflow": "#3ac486",
|
||||
"Nim": "#37775b",
|
||||
"Nit": "#009917",
|
||||
"Nix": "#7e7eff",
|
||||
"Nim": "#ffc200",
|
||||
"Nimrod": "#37775b",
|
||||
"Nit": "#0d8921",
|
||||
"Nix": "#7070ff",
|
||||
"Nu": "#c9df40",
|
||||
"Objective-C": "#438eff",
|
||||
"Objective-C++": "#6866fb",
|
||||
"Objective-J": "#ff0c5a",
|
||||
"NumPy": "#9C8AF9",
|
||||
"Nunjucks": "#3d8137",
|
||||
"OCaml": "#3be133",
|
||||
"ObjectScript": "#424893",
|
||||
"Objective-C": "#438eff",
|
||||
"Objective-C++": "#4886FC",
|
||||
"Objective-J": "#ff0c5a",
|
||||
"Odin": "#60AFFE",
|
||||
"Omgrofl": "#cabbff",
|
||||
"ooc": "#b0b77e",
|
||||
"Opal": "#f7ede0",
|
||||
"Oxygene": "#cdd0e3",
|
||||
"Oz": "#fab738",
|
||||
"OpenQASM": "#AA70FF",
|
||||
"Org": "#77aa99",
|
||||
"Oxygene": "#5a63a3",
|
||||
"Oz": "#fcaf3e",
|
||||
"P4": "#7055b5",
|
||||
"PAWN": "#dbb284",
|
||||
"PHP": "#4F5D95",
|
||||
"PLSQL": "#dad8d8",
|
||||
"Pan": "#cc0000",
|
||||
"Papyrus": "#6600cc",
|
||||
"Parrot": "#f3ca0a",
|
||||
"Pascal": "#E3F171",
|
||||
"Pascal": "#b0ce4e",
|
||||
"Pawn": "#dbb284",
|
||||
"Pep8": "#C76F5B",
|
||||
"Perl": "#0298c3",
|
||||
"Perl 6": "#0000fb",
|
||||
"PHP": "#4F5D95",
|
||||
"Perl6": "#0298c3",
|
||||
"PigLatin": "#fcd7de",
|
||||
"Pike": "#005390",
|
||||
"PLSQL": "#dad8d8",
|
||||
"Pike": "#066ab2",
|
||||
"PogoScript": "#d80074",
|
||||
"PostScript": "#da291c",
|
||||
"PowerBuilder": "#8f0f8d",
|
||||
"PowerShell": "#012456",
|
||||
"Processing": "#0096D8",
|
||||
"Prisma": "#0c344b",
|
||||
"Processing": "#2779ab",
|
||||
"Prolog": "#74283c",
|
||||
"Propeller Spin": "#7fa2a7",
|
||||
"Puppet": "#302B6D",
|
||||
"Propeller Spin": "#2b446d",
|
||||
"Pug": "#a86454",
|
||||
"Puppet": "#cc5555",
|
||||
"Pure Data": "#91de79",
|
||||
"PureBasic": "#5a6986",
|
||||
"PureScript": "#1D222D",
|
||||
"Python": "#3572A5",
|
||||
"q": "#0040cd",
|
||||
"PureScript": "#bcdc53",
|
||||
"Python": "#3581ba",
|
||||
"Q#": "#fed659",
|
||||
"QML": "#44a51c",
|
||||
"Qt Script": "#00b841",
|
||||
"Quake": "#882233",
|
||||
"R": "#198CE7",
|
||||
"Racket": "#3c5caa",
|
||||
"Ragel": "#9d5200",
|
||||
"R": "#198ce7",
|
||||
"RAML": "#77d9fb",
|
||||
"RUNOFF": "#665a4e",
|
||||
"Racket": "#ae17ff",
|
||||
"Ragel": "#9d5200",
|
||||
"Ragel in Ruby Host": "#ff9c2e",
|
||||
"Raku": "#0000fb",
|
||||
"Rascal": "#fffaa0",
|
||||
"ReScript": "#ed5051",
|
||||
"Reason": "#ff5847",
|
||||
"Rebol": "#358a5b",
|
||||
"Red": "#f50000",
|
||||
"Record Jar": "#0673ba",
|
||||
"Red": "#ee0000",
|
||||
"Ren'Py": "#ff7f7f",
|
||||
"Ring": "#2D54CB",
|
||||
"Riot": "#A71E49",
|
||||
"Roff": "#ecdebe",
|
||||
"Rouge": "#cc0088",
|
||||
"Ruby": "#701516",
|
||||
"RUNOFF": "#665a4e",
|
||||
"Rust": "#dea584",
|
||||
"SAS": "#1E90FF",
|
||||
"SCSS": "#c6538c",
|
||||
"SQF": "#FFCB1F",
|
||||
"SRecode Template": "#348a34",
|
||||
"SVG": "#ff9900",
|
||||
"SaltStack": "#646464",
|
||||
"SAS": "#B34936",
|
||||
"Scala": "#c22d40",
|
||||
"Sass": "#a53b70",
|
||||
"Scala": "#7dd3b0",
|
||||
"Scaml": "#bd181a",
|
||||
"Scheme": "#1e4aec",
|
||||
"sed": "#64b970",
|
||||
"Self": "#0579aa",
|
||||
"Shell": "#89e051",
|
||||
"Shell": "#5861ce",
|
||||
"Shen": "#120F14",
|
||||
"Slash": "#007eff",
|
||||
"Slice": "#003fa2",
|
||||
"Slim": "#ff8877",
|
||||
"SmPL": "#c94949",
|
||||
"Smalltalk": "#596706",
|
||||
"Solidity": "#AA6746",
|
||||
"SourcePawn": "#5c7611",
|
||||
"SQF": "#3F3F3F",
|
||||
"SourcePawn": "#f69e1d",
|
||||
"Squirrel": "#800000",
|
||||
"SRecode Template": "#348a34",
|
||||
"Stan": "#b2011d",
|
||||
"Standard ML": "#dc566d",
|
||||
"Starlark": "#76d275",
|
||||
"Stylus": "#ff6347",
|
||||
"SuperCollider": "#46390b",
|
||||
"Svelte": "#ff3e00",
|
||||
"Swift": "#ffac45",
|
||||
"SystemVerilog": "#DAE1C2",
|
||||
"Tcl": "#e4cc98",
|
||||
"Terra": "#00004c",
|
||||
"TeX": "#3D6117",
|
||||
"SystemVerilog": "#343761",
|
||||
"TI Program": "#A0AA87",
|
||||
"Turing": "#cf142b",
|
||||
"TypeScript": "#2b7489",
|
||||
"Tcl": "#e4cc98",
|
||||
"TeX": "#3D6117",
|
||||
"Terra": "#00004c",
|
||||
"Turing": "#45f715",
|
||||
"Twig": "#c1d026",
|
||||
"TypeScript": "#31859c",
|
||||
"Unified Parallel C": "#755223",
|
||||
"Uno": "#9933cc",
|
||||
"UnrealScript": "#a54c4d",
|
||||
"Vala": "#fbe5cd",
|
||||
"VCL": "#148AA8",
|
||||
"Verilog": "#b2b7f8",
|
||||
"VHDL": "#adb2cb",
|
||||
"V": "#4f87c4",
|
||||
"VBA": "#867db1",
|
||||
"VBScript": "#15dcdc",
|
||||
"VCL": "#0298c3",
|
||||
"VHDL": "#543978",
|
||||
"Vala": "#ee7d06",
|
||||
"Verilog": "#848bf3",
|
||||
"Vim script": "#199f4b",
|
||||
"VimL": "#199c4b",
|
||||
"Visual Basic": "#945db7",
|
||||
"Volt": "#1F1F1F",
|
||||
"Visual Basic .NET": "#945db7",
|
||||
"Volt": "#0098db",
|
||||
"Vue": "#2c3e50",
|
||||
"wdl": "#42f1f4",
|
||||
"Web Ontology Language": "#3994bc",
|
||||
"WebAssembly": "#04133b",
|
||||
"wisp": "#7582D1",
|
||||
"Wollok": "#a23738",
|
||||
"X10": "#4B6BEF",
|
||||
"xBase": "#403a40",
|
||||
"XC": "#99DA07",
|
||||
"XQuery": "#5232e7",
|
||||
"XQuery": "#2700e2",
|
||||
"XSLT": "#EB8CEB",
|
||||
"Yacc": "#4B6C4B",
|
||||
"YAML": "#cb171e",
|
||||
"YARA": "#220000",
|
||||
"YASnippet": "#32AB90",
|
||||
"Yacc": "#4B6C4B",
|
||||
"ZAP": "#0d665e",
|
||||
"ZIL": "#dc75e5",
|
||||
"ZenScript": "#00BCD1",
|
||||
"Zephir": "#118f9e",
|
||||
"Zig": "#ec915c",
|
||||
"ZIL": "#dc75e5"
|
||||
"cpp": "#f34b7d",
|
||||
"eC": "#913960",
|
||||
"edn": "#db5855",
|
||||
"mIRC Script": "#3d57c3",
|
||||
"mcfunction": "#E22837",
|
||||
"nesC": "#ffce3b",
|
||||
"ooc": "#b0b77e",
|
||||
"q": "#0040cd",
|
||||
"sed": "#64b970",
|
||||
"wdl": "#42f1f4",
|
||||
"wisp": "#7582D1",
|
||||
"xBase": "#3a4040",
|
||||
"Other": "#1f9aef"
|
||||
},
|
||||
"editors": {
|
||||
"Adobe XD": "#fd27bc",
|
||||
"Android Studio": "#99cd00",
|
||||
"AppCode": "#04dbde",
|
||||
"Aptana": "#ec8623",
|
||||
"Atom": "#49b77e",
|
||||
"Azure Data Studio": "#0271c6",
|
||||
"Blender": "#fb8007",
|
||||
"BlueJ": "#5d89af",
|
||||
"Brackets": "#067dc3",
|
||||
"Chrome": "#fdd308",
|
||||
"CLion": "#14c9a5",
|
||||
"Cloud9": "#25a6d9",
|
||||
"Coda": "#3e8e1c",
|
||||
"Code: :Blocks": "#d0ce71",
|
||||
"Code::Blocks": "#d0ce71",
|
||||
"CodeLite": "#1892e5",
|
||||
"CodeTasty": "#7368a8",
|
||||
"DataGrip": "#907cf2",
|
||||
"DBeaver": "#897363",
|
||||
"Eclipse": "#443582",
|
||||
"Emacs": "#8c76c3",
|
||||
"Embarcadero Delphi": "#d9242a",
|
||||
"EmEditor": "#ed3103",
|
||||
"Eric": "#423f13",
|
||||
"Excel": "#0f753c",
|
||||
"Figma": "#c7b9ff",
|
||||
"Firefox": "#d96527",
|
||||
"Flash Builder": "#aca3a4",
|
||||
"Geany": "#fbec75",
|
||||
"Gedit": "#872114",
|
||||
"GoLand": "#bd4ffc",
|
||||
"HBuilder X": "#1ba334",
|
||||
"IntelliJ IDEA": "#2876e1",
|
||||
"IntelliJ": "#2876e1",
|
||||
"Kakoune": "#dd5f4a",
|
||||
"Kate": "#3f4040",
|
||||
"KDevelop": "#22a273",
|
||||
"Komodo": "#fcb414",
|
||||
"Light Table": "#007ac1",
|
||||
"MacRabbit Espresso": "#e6593f",
|
||||
"Micro": "#2c3494",
|
||||
"MonoDevelop": "#6185b3",
|
||||
"MySQL Workbench": "#245279",
|
||||
"Neovim": "#068304",
|
||||
"NetBeans": "#f1f6e2",
|
||||
"Notepad++": "#9ecf54",
|
||||
"Nova": "#ff054a",
|
||||
"Onivim": "#ee848e",
|
||||
"Photoshop": "#0a0054",
|
||||
"PhpStorm": "#d93ac1",
|
||||
"PowerPoint": "#c6421f",
|
||||
"Processing": "#6a7152",
|
||||
"PyCharm": "#d2ee5c",
|
||||
"Pymakr": "#323d4f",
|
||||
"QtCreator": "#7fc342",
|
||||
"Rider": "#f7a415",
|
||||
"RStudio": "#2369c7",
|
||||
"RubyMine": "#ff6336",
|
||||
"Sketch": "#fdad00",
|
||||
"SlickEdit": "#57ca57",
|
||||
"Spyder": "#ee181e",
|
||||
"SQL Server Management Studio": "#ffb901",
|
||||
"Sublime Text": "#ff9800",
|
||||
"Terminal": "#133f1c",
|
||||
"TeXstudio": "#652d96",
|
||||
"TextMate": "#822b7a",
|
||||
"Unity": "#222d36",
|
||||
"Vim": "#068304",
|
||||
"Visual Studio": "#9460cd",
|
||||
"VS Code": "#027acd",
|
||||
"VSCode": "#027acd",
|
||||
"WebMatrix": "#aeaeae",
|
||||
"WebStorm": "#00c6d7",
|
||||
"Wing": "#b3b3b3",
|
||||
"Word": "#0f4091",
|
||||
"WPS Office": "#fc6143",
|
||||
"Xamarin": "#3598db",
|
||||
"Xcode": "#3fa7e4"
|
||||
},
|
||||
"operating_systems": {
|
||||
"Linux": "#f0b912",
|
||||
"Windows": "#00b7ee",
|
||||
"Mac": "#4d66cb"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,3 +22,8 @@ services:
|
||||
POSTGRES_USER: "wakapi"
|
||||
POSTGRES_PASSWORD: "choose-a-password"
|
||||
POSTGRES_DB: "wakapi"
|
||||
volumes:
|
||||
- wakapi-db-data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
wakapi-db-data: {}
|
||||
|
53
etc/wakapi.service
Normal file
53
etc/wakapi.service
Normal file
@ -0,0 +1,53 @@
|
||||
[Unit]
|
||||
Description=Wakapi
|
||||
StartLimitIntervalSec=400
|
||||
StartLimitBurst=3
|
||||
|
||||
# Optional, in case you're running MySQL / Postgres with Systemd, too
|
||||
Requires=mysql.service
|
||||
After=mysql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
# Assuming Wakapi executable is under /opt/wakapi and config file at /etc
|
||||
# Feel free to change this
|
||||
WorkingDirectory=/opt/wakapi
|
||||
ExecStart=/opt/wakapi/wakapi -config /etc/wakapi.yml
|
||||
|
||||
# Environment variables, see README for more
|
||||
Environment=WAKAPI_DB_HOST=localhost
|
||||
Environment=WAKAPI_DB_USER=wakapi
|
||||
Environment=WAKAPI_DB_NAME=wakapi
|
||||
Environment=WAKAPI_DB_PASSWORD=secretpassword
|
||||
Environment=WAKAPI_PASSWORD_SALT=somerandomstring
|
||||
|
||||
# TODO: Use Systemd's credentials management (https://systemd.io/CREDENTIALS/) introduced in v247 (%d syntax in v250) once more established
|
||||
|
||||
# sudo groupadd wakapi
|
||||
# sudo useradd -g wakapi wakapi
|
||||
User=wakapi
|
||||
Group=wakapi
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=90
|
||||
|
||||
# Security hardening
|
||||
PrivateTmp=true
|
||||
PrivateUsers=true
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectControlGroups=true
|
||||
PrivateDevices=true
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
ProtectClock=true
|
||||
RestrictSUIDSGID=true
|
||||
ProtectHostname=true
|
||||
ProtectProc=invisible
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
96
go.mod
96
go.mod
@ -1,41 +1,87 @@
|
||||
module github.com/muety/wakapi
|
||||
|
||||
go 1.16
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
codeberg.org/Codeberg/avatars v0.0.0-20211228163022-8da63012fe69
|
||||
github.com/BurntSushi/toml v0.4.1 // indirect
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
|
||||
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac
|
||||
github.com/emersion/go-smtp v0.15.0
|
||||
codeberg.org/Codeberg/avatars v1.0.0
|
||||
github.com/duke-git/lancet/v2 v2.1.13
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
||||
github.com/emersion/go-smtp v0.16.0
|
||||
github.com/emvi/logbuch v1.2.0
|
||||
github.com/felixge/httpsnoop v1.0.2 // indirect
|
||||
github.com/getsentry/sentry-go v0.11.0
|
||||
github.com/go-co-op/gocron v1.11.0
|
||||
github.com/go-openapi/spec v0.20.2 // indirect
|
||||
github.com/getsentry/sentry-go v0.15.0
|
||||
github.com/glebarez/sqlite v1.6.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/schema v1.2.0
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/jackc/pgx/v4 v4.14.1 // indirect
|
||||
github.com/jinzhu/configor v1.2.1
|
||||
github.com/jinzhu/now v1.1.4 // indirect
|
||||
github.com/leandro-lugaresi/hub v1.1.1
|
||||
github.com/lpar/gzipped/v2 v2.0.2
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||
github.com/lpar/gzipped/v2 v2.1.0
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/muety/artifex/v2 v2.0.1-0.20221201142708-74e7d3f6feaf
|
||||
github.com/narqo/go-badge v0.0.0-20221212191103-ba83bed45a1a
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/swaggo/swag v1.7.0
|
||||
go.uber.org/atomic v1.9.0
|
||||
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/tools v0.1.0 // indirect
|
||||
gorm.io/driver/mysql v1.2.1
|
||||
gorm.io/driver/postgres v1.2.3
|
||||
gorm.io/driver/sqlite v1.2.6
|
||||
gorm.io/gorm v1.22.4
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/stripe/stripe-go/v74 v74.5.0
|
||||
github.com/swaggo/http-swagger v1.3.3
|
||||
github.com/swaggo/swag v1.8.9
|
||||
go.uber.org/atomic v1.10.0
|
||||
golang.org/x/crypto v0.5.0
|
||||
golang.org/x/sync v0.1.0
|
||||
gorm.io/driver/mysql v1.4.5
|
||||
gorm.io/driver/postgres v1.4.6
|
||||
gorm.io/driver/sqlite v1.4.4
|
||||
gorm.io/gorm v1.24.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/glebarez/go-sqlite v1.20.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/spec v0.20.8 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.13.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgtype v1.13.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.17.2 // indirect
|
||||
github.com/jackc/pgx/v5 v5.2.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/swaggo/files v1.0.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230116083435-1de6713980de // indirect
|
||||
golang.org/x/image v0.3.0 // indirect
|
||||
golang.org/x/net v0.5.0 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/text v0.6.0 // indirect
|
||||
golang.org/x/tools v0.5.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.2 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.20.2 // indirect
|
||||
)
|
||||
|
483
go.sum
483
go.sum
@ -1,104 +1,92 @@
|
||||
codeberg.org/Codeberg/avatars v0.0.0-20211228163022-8da63012fe69 h1:/XvI42KX57UTpeIOIt7IfM+pmEFTL8FGtiIUGcGDOIU=
|
||||
codeberg.org/Codeberg/avatars v0.0.0-20211228163022-8da63012fe69/go.mod h1:ML/htpPRb3+owhkm4+qG2ZrXnk5WXaQLASOZ5GLCPi8=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
codeberg.org/Codeberg/avatars v1.0.0 h1:MRx5QxuT/oVCcPvC5rXwgwWKD7hc6J0GnZ0Kl67lYEM=
|
||||
codeberg.org/Codeberg/avatars v1.0.0/go.mod h1:ML/htpPRb3+owhkm4+qG2ZrXnk5WXaQLASOZ5GLCPi8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
|
||||
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
|
||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
|
||||
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
||||
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/duke-git/lancet/v2 v2.1.10 h1:q6YKhbYg6KChBS+T41e/IhK+sTDPVk2wRhWLTevCeuY=
|
||||
github.com/duke-git/lancet/v2 v2.1.10/go.mod h1:5Nawyf/bK783rCiHyVkZLx+jj8028oVVjLOrC21ZONA=
|
||||
github.com/duke-git/lancet/v2 v2.1.13 h1:KOCCVrfh4pjuwl6td5MQ4OqvV73qFdoGxv20HWmyPaM=
|
||||
github.com/duke-git/lancet/v2 v2.1.13/go.mod h1:hNcc06mV7qr+crH/0nP+rlC3TB0Q9g5OrVnO8/TGD4c=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFVgAHfjlLyqMewry25Rz7jWnVoh4Ggs=
|
||||
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
|
||||
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8=
|
||||
github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/emvi/logbuch v1.2.0 h1:Bw0jQH1Dbs+oIygZBNx/2Ub1igXRFtKQrIMRrZdVFJM=
|
||||
github.com/emvi/logbuch v1.2.0/go.mod h1:hFxe0XQOFl76SkE/f0Pt5oQbXRZtyGa8EroBrrbQHuc=
|
||||
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o=
|
||||
github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/getsentry/sentry-go v0.11.0 h1:qro8uttJGvNAMr5CLcFI9CHR0aDzXl0Vs3Pmw/oTPg8=
|
||||
github.com/getsentry/sentry-go v0.11.0/go.mod h1:KBQIxiZAetw62Cj8Ri964vAEWVdgfaUCn30Q3bCvANo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-co-op/gocron v1.11.0 h1:ujOMubCpGcTxnnR/9vJIPIEpgwuAjbueAYqJRNr+nHg=
|
||||
github.com/go-co-op/gocron v1.11.0/go.mod h1:qtlsoMpHlSdIZ3E/xuZzrrAbeX3u5JtPvWf2TcdutU0=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/getsentry/sentry-go v0.15.0 h1:CP9bmA7pralrVUedYZsmIHWpq/pBtXTSew7xvVpfLaA=
|
||||
github.com/getsentry/sentry-go v0.15.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2HtRckJk9XVxJ9I=
|
||||
github.com/getsentry/sentry-go v0.17.0 h1:UustVWnOoDFHBS7IJUB2QK/nB5pap748ZEp0swnQJak=
|
||||
github.com/getsentry/sentry-go v0.17.0/go.mod h1:B82dxtBvxG0KaPD8/hfSV+VcHD+Lg/xUS4JuQn1P4cM=
|
||||
github.com/glebarez/go-sqlite v1.19.1/go.mod h1:9AykawGIyIcxoSfpYWiX1SgTNHTNsa/FVc75cDkbp4M=
|
||||
github.com/glebarez/go-sqlite v1.19.5 h1:krEVjICcImFNi+X81GmEkSe/brhzLL3Csbkb/ihi8sI=
|
||||
github.com/glebarez/go-sqlite v1.19.5/go.mod h1:IjVxx3ezfL9clKLLSzVgv2sGZe28yIa116YyLTIvp84=
|
||||
github.com/glebarez/go-sqlite v1.20.0 h1:6D9uRXq3Kd+W7At+hOU2eIAeahv6qcYfO8jzmvb4Dr8=
|
||||
github.com/glebarez/go-sqlite v1.20.0/go.mod h1:uTnJoqtwMQjlULmljLT73Cg7HB+2X6evsBHODyyq1ak=
|
||||
github.com/glebarez/sqlite v1.5.0 h1:+8LAEpmywqresSoGlqjjT+I9m4PseIM3NcerIJ/V7mk=
|
||||
github.com/glebarez/sqlite v1.5.0/go.mod h1:0wzXzTvfVJIN2GqRhCdMbnYd+m+aH5/QV7B30rM6NgY=
|
||||
github.com/glebarez/sqlite v1.6.0 h1:ZpvDLv4zBi2cuuQPitRiVz/5Uh6sXa5d8eBu0xNTpAo=
|
||||
github.com/glebarez/sqlite v1.6.0/go.mod h1:6D6zPU/HTrFlYmVDKqBJlmQvma90P6r7sRRdkUUZOYk=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
||||
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
|
||||
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
||||
github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA=
|
||||
github.com/go-openapi/spec v0.20.2 h1:pFPUZsiIbZ20kLUcuCGeuQWG735fPMxW7wHF9BWlnQU=
|
||||
github.com/go-openapi/spec v0.20.2/go.mod h1:RW6Xcbs6LOyWLU/mXGdzn2Qc+3aj+ASfI7rvSZh1Vls=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI=
|
||||
github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU=
|
||||
github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY=
|
||||
github.com/go-openapi/swag v0.19.13 h1:233UVgMy1DlmCYYfOiFpta6e2urloh+sEs5id6lyzog=
|
||||
github.com/go-openapi/swag v0.19.13/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
@ -107,19 +95,11 @@ github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
|
||||
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
|
||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
|
||||
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
|
||||
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
|
||||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
@ -131,8 +111,8 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8=
|
||||
github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
|
||||
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
@ -149,63 +129,58 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns=
|
||||
github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
|
||||
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
||||
github.com/jackc/pgtype v1.9.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgtype v1.9.1 h1:MJc2s0MFS8C3ok1wQTdQxWuXQcB6+HwAm5x1CzW7mf0=
|
||||
github.com/jackc/pgtype v1.9.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
|
||||
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgtype v1.13.0 h1:XkIc7A+1BmZD19bB2NxrtjJweHxQ9agqvM+9URc68Cg=
|
||||
github.com/jackc/pgtype v1.13.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||
github.com/jackc/pgx/v4 v4.14.0/go.mod h1:jT3ibf/A0ZVCp89rtCIN0zCJxcE74ypROmHEZYsG/j8=
|
||||
github.com/jackc/pgx/v4 v4.14.1 h1:71oo1KAGI6mXhLiTMn6iDFcp3e7+zon/capWjl2OEFU=
|
||||
github.com/jackc/pgx/v4 v4.14.1/go.mod h1:RgDuE4Z34o7XE92RpLsvFiOEfrAUT0Xt2KxvX73W06M=
|
||||
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
|
||||
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
|
||||
github.com/jackc/pgx/v5 v5.2.0 h1:NdPpngX0Y6z6XDFKqmFQaE+bCtkqzvQIOt1wvBlAqs8=
|
||||
github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels=
|
||||
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
|
||||
github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
|
||||
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
|
||||
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
|
||||
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
|
||||
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
|
||||
github.com/kevinpollet/nego v0.0.0-20200324111829-b3061ca9dd9d h1:BaIpmhcqpBnz4+NZjUjVGxKNA+/E7ovKsjmwqjXcGYc=
|
||||
github.com/kevinpollet/nego v0.0.0-20200324111829-b3061ca9dd9d/go.mod h1:3FSWkzk9h42opyV0o357Fq6gsLF/A6MI/qOca9kKobY=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 h1:Pdirg1gwhEcGjMLyuSxGn9664p+P8J9SrfMgpFwrDyg=
|
||||
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43/go.mod h1:ahLMuLCUyDdXqtqGyuwGev7/PGtO7r7ocvdwDuEN/3E=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/leandro-lugaresi/hub v1.1.1 h1:zqp0HzFvj4HtqjMBXM2QF17o6PNmR8MJOChgeKl/aw8=
|
||||
github.com/leandro-lugaresi/hub v1.1.1/go.mod h1:XEFWanhHv6Rt3XlteHMxuNDYi8dJcpJjodpqkU+BtIo=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
@ -213,121 +188,99 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lpar/gzipped/v2 v2.0.2 h1:y7FjyTH07f8dX0YQ5o0sg2DTbRnmS3oT1pUvxViQ//o=
|
||||
github.com/lpar/gzipped/v2 v2.0.2/go.mod h1:qb7pLOGFgqz5w9xGGiiRFPxuGZ7GqWEuXUKXSbgonkQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/lpar/gzipped/v2 v2.1.0 h1:87/ug239roEqXLVOnXZg6NjDfFvMwmkGTKnFWJPUA9U=
|
||||
github.com/lpar/gzipped/v2 v2.1.0/go.mod h1:G3UlFoFYzjCx6NV4zDmD1BIWMNBaJuKoUvxrEWJuZ3Y=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/muety/artifex/v2 v2.0.1-0.20221201142708-74e7d3f6feaf h1:zd7IU9rxVMl2FBwSwiWCUh6s0TkPKgOU6GyVBciNdlo=
|
||||
github.com/muety/artifex/v2 v2.0.1-0.20221201142708-74e7d3f6feaf/go.mod h1:eElbcdMwTDc7Wzl7A46IopgkC6a9nV7jOB6Mw8r0waE=
|
||||
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e h1:bR8DQ4ZfItytLJwRlrLOPUHd5z18V6tECwYQFy8W+8g=
|
||||
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e/go.mod h1:m9BzkaxwU4IfPQi9ko23cmuFltayFe8iS0dlRlnEWiM=
|
||||
github.com/narqo/go-badge v0.0.0-20221212191103-ba83bed45a1a h1:G6Kjw+HNpJUZY1bfBkd8XOZ7nuDWmXLaJukeiM2Xv7o=
|
||||
github.com/narqo/go-badge v0.0.0-20221212191103-ba83bed45a1a/go.mod h1:m9BzkaxwU4IfPQi9ko23cmuFltayFe8iS0dlRlnEWiM=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa h1:tEkEyxYeZ43TR55QU/hsIt9aRGBxbgGuz9CGykjvogY=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E=
|
||||
github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stripe/stripe-go/v74 v74.3.0 h1:8ymGwZvMnpWMCRNomc9dVGcJ5j8L/ubwhQvpIpcmcOA=
|
||||
github.com/stripe/stripe-go/v74 v74.3.0/go.mod h1:5PoXNp30AJ3tGq57ZcFuaMylzNi8KpwlrYAFmO1fHZw=
|
||||
github.com/stripe/stripe-go/v74 v74.5.0 h1:YyqTvVQdS34KYGCfVB87EMn9eDV3FCFkSwfdOQhiVL4=
|
||||
github.com/stripe/stripe-go/v74 v74.5.0/go.mod h1:5PoXNp30AJ3tGq57ZcFuaMylzNi8KpwlrYAFmO1fHZw=
|
||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY=
|
||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||
github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4=
|
||||
github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc=
|
||||
github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc=
|
||||
github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo=
|
||||
github.com/swaggo/swag v1.8.8 h1:/GgJmrJ8/c0z4R4hoEPZ5UeEhVGdvsII4JbVDLbR7Xc=
|
||||
github.com/swaggo/swag v1.8.8/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
|
||||
github.com/swaggo/swag v1.8.9 h1:kHtaBe/Ob9AZzAANfcn5c6RyCke9gG9QpH0jky0I/sA=
|
||||
github.com/swaggo/swag v1.8.9/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
@ -335,82 +288,100 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4=
|
||||
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
||||
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/exp v0.0.0-20230116083435-1de6713980de h1:DBWn//IJw30uYCgERoxCg84hWtA97F4wMiKOIh00Uf0=
|
||||
golang.org/x/exp v0.0.0-20230116083435-1de6713980de/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
|
||||
golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
|
||||
golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg=
|
||||
golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
@ -418,46 +389,98 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
|
||||
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
|
||||
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.2.1 h1:h+3f1l9Ng2C072Y2tIiLgPpWN78r1KXL7bHJ0nTjlhU=
|
||||
gorm.io/driver/mysql v1.2.1/go.mod h1:qsiz+XcAyMrS6QY+X3M9R6b/lKM1imKmcuK9kac5LTo=
|
||||
gorm.io/driver/postgres v1.2.3 h1:f4t0TmNMy9gh3TU2PX+EppoA6YsgFnyq8Ojtddb42To=
|
||||
gorm.io/driver/postgres v1.2.3/go.mod h1:pJV6RgYQPG47aM1f0QeOzFH9HxQc8JcmAgjRCgS0wjs=
|
||||
gorm.io/driver/sqlite v1.2.6 h1:SStaH/b+280M7C8vXeZLz/zo9cLQmIGwwj3cSj7p6l4=
|
||||
gorm.io/driver/sqlite v1.2.6/go.mod h1:gyoX0vHiiwi0g49tv+x2E7l8ksauLK0U/gShcdUsjWY=
|
||||
gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||
gorm.io/gorm v1.22.4 h1:8aPcyEJhY0MAt8aY6Dc524Pn+pO29K+ydu+e/cXSpQM=
|
||||
gorm.io/gorm v1.22.4/go.mod h1:1aeVC+pe9ZmvKZban/gW4QPra7PRoTEssyc922qCAkk=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.4.4 h1:MX0K9Qvy0Na4o7qSC/YI7XxqUw5KDw01umqgID+svdQ=
|
||||
gorm.io/driver/mysql v1.4.4/go.mod h1:BCg8cKI+R0j/rZRQxeKis/forqRwRSYOR8OM3Wo6hOM=
|
||||
gorm.io/driver/mysql v1.4.5 h1:u1lytId4+o9dDaNcPCFzNv7h6wvmc92UjNk3z8enSBU=
|
||||
gorm.io/driver/mysql v1.4.5/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
|
||||
gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc=
|
||||
gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg=
|
||||
gorm.io/driver/postgres v1.4.6 h1:1FPESNXqIKG5JmraaH2bfCVlMQ7paLoCreFxDtqzwdc=
|
||||
gorm.io/driver/postgres v1.4.6/go.mod h1:UJChCNLFKeBqQRE+HrkFUbKbq9idPXmTOk2u4Wok8S4=
|
||||
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
||||
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
|
||||
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
gorm.io/gorm v1.24.2 h1:9wR6CFD+G8nOusLdvkZelOEhpJVwwHzpQOUM+REd6U0=
|
||||
gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
gorm.io/gorm v1.24.3 h1:WL2ifUmzR/SLp85CSURAfybcHnGZ+yLSGSxgYXlFBHg=
|
||||
gorm.io/gorm v1.24.3/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
|
||||
modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
|
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0=
|
||||
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
|
||||
modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
|
||||
modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA=
|
||||
modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0=
|
||||
modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
|
||||
modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
|
||||
modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
|
||||
modernc.org/libc v1.21.5 h1:xBkU9fnHV+hvZuPSRszN0AXDG4M7nwPLwTWwkYcvLCI=
|
||||
modernc.org/libc v1.21.5/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
|
||||
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
|
||||
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
|
||||
modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
|
||||
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.19.1/go.mod h1:UfQ83woKMaPW/ZBruK0T7YaFCrI+IE0LeWVY6pmnVms=
|
||||
modernc.org/sqlite v1.19.5/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw=
|
||||
modernc.org/sqlite v1.20.0 h1:80zmD3BGkm8BZ5fUi/4lwJQHiO3GXgIUvZRXpoIfROY=
|
||||
modernc.org/sqlite v1.20.0/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw=
|
||||
modernc.org/sqlite v1.20.2 h1:9AaVzJH1Yf0u9iOZRjjuvqxLoGqybqVFbAUC5rvi9u8=
|
||||
modernc.org/sqlite v1.20.2/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.14.0/go.mod h1:gQ7c1YPMvryCHCcmf8acB6VPabE59QBeuRQLL7cTUlM=
|
||||
modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE=
|
||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.6.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
|
||||
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
|
||||
|
@ -1,9 +1,8 @@
|
||||
package utils
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/muety/wakapi/config"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -41,15 +40,10 @@ func FormatDateHuman(date time.Time) string {
|
||||
return date.Format("Mon, 02 Jan 2006")
|
||||
}
|
||||
|
||||
func Add(i, j int) int {
|
||||
return i + j
|
||||
}
|
||||
|
||||
func ParseUserAgent(ua string) (string, string, error) {
|
||||
re := regexp.MustCompile(`(?iU)^wakatime\/(?:v?[\d+.]+|unset)\s\((\w+)-.*\)\s.+\s([^\/\s]+)-wakatime\/.+$`)
|
||||
groups := re.FindAllStringSubmatch(ua, -1)
|
||||
if len(groups) == 0 || len(groups[0]) != 3 {
|
||||
return "", "", errors.New("failed to parse user agent string")
|
||||
}
|
||||
return groups[0][1], groups[0][2], nil
|
||||
func FmtWakatimeDuration(d time.Duration) string {
|
||||
d = d.Round(time.Minute)
|
||||
h := d / time.Hour
|
||||
d -= h * time.Hour
|
||||
m := d / time.Minute
|
||||
return fmt.Sprintf("%d hrs %d mins", h, m)
|
||||
}
|
4
helpers/helpers.go
Normal file
4
helpers/helpers.go
Normal file
@ -0,0 +1,4 @@
|
||||
package helpers
|
||||
|
||||
// helpers are different from utils in that they contain wakapi-specific utility functions
|
||||
// also, helpers may depend on the config package, while utils must be entirely static
|
30
helpers/http.go
Normal file
30
helpers/http.go
Normal file
@ -0,0 +1,30 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func ExtractCookieAuth(r *http.Request, config *config.Config) (username *string, err error) {
|
||||
cookie, err := r.Cookie(models.AuthCookieKey)
|
||||
if err != nil {
|
||||
return nil, errors.New("missing authentication")
|
||||
}
|
||||
|
||||
if err := config.Security.SecureCookie.Decode(models.AuthCookieKey, cookie.Value, &username); err != nil {
|
||||
return nil, errors.New("cookie is invalid")
|
||||
}
|
||||
|
||||
return username, nil
|
||||
}
|
||||
|
||||
func RespondJSON(w http.ResponseWriter, r *http.Request, status int, object interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if err := json.NewEncoder(w).Encode(object); err != nil {
|
||||
config.Log().Request(r).Error("error while writing json response: %v", err)
|
||||
}
|
||||
}
|
@ -1,76 +1,13 @@
|
||||
package utils
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ParseInterval(interval string) (*models.IntervalKey, error) {
|
||||
for _, i := range models.AllIntervals {
|
||||
if i.HasAlias(interval) {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("not a valid interval")
|
||||
}
|
||||
|
||||
func MustResolveIntervalRawTZ(interval string, tz *time.Location) (from, to time.Time) {
|
||||
_, from, to = ResolveIntervalRawTZ(interval, tz)
|
||||
return from, to
|
||||
}
|
||||
|
||||
func ResolveIntervalRawTZ(interval string, tz *time.Location) (err error, from, to time.Time) {
|
||||
parsed, err := ParseInterval(interval)
|
||||
if err != nil {
|
||||
return err, time.Time{}, time.Time{}
|
||||
}
|
||||
return ResolveIntervalTZ(parsed, tz)
|
||||
}
|
||||
|
||||
func ResolveIntervalTZ(interval *models.IntervalKey, tz *time.Location) (err error, from, to time.Time) {
|
||||
now := time.Now().In(tz)
|
||||
to = now
|
||||
|
||||
switch interval {
|
||||
case models.IntervalToday:
|
||||
from = StartOfToday(tz)
|
||||
case models.IntervalYesterday:
|
||||
from = StartOfToday(tz).Add(-24 * time.Hour)
|
||||
to = StartOfToday(tz)
|
||||
case models.IntervalThisWeek:
|
||||
from = StartOfThisWeek(tz)
|
||||
case models.IntervalLastWeek:
|
||||
from = StartOfThisWeek(tz).AddDate(0, 0, -7)
|
||||
to = StartOfThisWeek(tz)
|
||||
case models.IntervalThisMonth:
|
||||
from = StartOfThisMonth(tz)
|
||||
case models.IntervalLastMonth:
|
||||
from = StartOfThisMonth(tz).AddDate(0, -1, 0)
|
||||
to = StartOfThisMonth(tz)
|
||||
case models.IntervalThisYear:
|
||||
from = StartOfThisYear(tz)
|
||||
case models.IntervalPast7Days:
|
||||
from = now.AddDate(0, 0, -7)
|
||||
case models.IntervalPast7DaysYesterday:
|
||||
from = StartOfToday(tz).AddDate(0, 0, -1).AddDate(0, 0, -7)
|
||||
to = StartOfToday(tz).AddDate(0, 0, -1)
|
||||
case models.IntervalPast14Days:
|
||||
from = now.AddDate(0, 0, -14)
|
||||
case models.IntervalPast30Days:
|
||||
from = now.AddDate(0, 0, -30)
|
||||
case models.IntervalPast12Months:
|
||||
from = now.AddDate(0, -12, 0)
|
||||
case models.IntervalAny:
|
||||
from = time.Time{}
|
||||
default:
|
||||
err = errors.New("invalid interval")
|
||||
}
|
||||
|
||||
return err, from, to
|
||||
}
|
||||
|
||||
func ParseSummaryParams(r *http.Request) (*models.SummaryParams, error) {
|
||||
user := extractUser(r)
|
||||
params := r.URL.Query()
|
||||
@ -142,3 +79,69 @@ func extractUser(r *http.Request) *models.User {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseInterval(interval string) (*models.IntervalKey, error) {
|
||||
for _, i := range models.AllIntervals {
|
||||
if i.HasAlias(interval) {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("not a valid interval")
|
||||
}
|
||||
|
||||
func MustResolveIntervalRawTZ(interval string, tz *time.Location) (from, to time.Time) {
|
||||
_, from, to = ResolveIntervalRawTZ(interval, tz)
|
||||
return from, to
|
||||
}
|
||||
|
||||
func ResolveIntervalRawTZ(interval string, tz *time.Location) (err error, from, to time.Time) {
|
||||
parsed, err := ParseInterval(interval)
|
||||
if err != nil {
|
||||
return err, time.Time{}, time.Time{}
|
||||
}
|
||||
return ResolveIntervalTZ(parsed, tz)
|
||||
}
|
||||
|
||||
func ResolveIntervalTZ(interval *models.IntervalKey, tz *time.Location) (err error, from, to time.Time) {
|
||||
now := time.Now().In(tz)
|
||||
to = now
|
||||
|
||||
switch interval {
|
||||
case models.IntervalToday:
|
||||
from = utils.BeginOfToday(tz)
|
||||
case models.IntervalYesterday:
|
||||
from = utils.BeginOfToday(tz).Add(-24 * time.Hour)
|
||||
to = utils.BeginOfToday(tz)
|
||||
case models.IntervalThisWeek:
|
||||
from = utils.BeginOfThisWeek(tz)
|
||||
case models.IntervalLastWeek:
|
||||
from = utils.BeginOfThisWeek(tz).AddDate(0, 0, -7)
|
||||
to = utils.BeginOfThisWeek(tz)
|
||||
case models.IntervalThisMonth:
|
||||
from = utils.BeginOfThisMonth(tz)
|
||||
case models.IntervalLastMonth:
|
||||
from = utils.BeginOfThisMonth(tz).AddDate(0, -1, 0)
|
||||
to = utils.BeginOfThisMonth(tz)
|
||||
case models.IntervalThisYear:
|
||||
from = utils.BeginOfThisYear(tz)
|
||||
case models.IntervalPast7Days:
|
||||
from = now.AddDate(0, 0, -7)
|
||||
case models.IntervalPast7DaysYesterday:
|
||||
from = utils.BeginOfToday(tz).AddDate(0, 0, -1).AddDate(0, 0, -7)
|
||||
to = utils.BeginOfToday(tz).AddDate(0, 0, -1)
|
||||
case models.IntervalPast14Days:
|
||||
from = now.AddDate(0, 0, -14)
|
||||
case models.IntervalPast30Days:
|
||||
from = now.AddDate(0, 0, -30)
|
||||
case models.IntervalPast6Months:
|
||||
from = now.AddDate(0, -6, 0)
|
||||
case models.IntervalPast12Months:
|
||||
from = now.AddDate(0, -12, 0)
|
||||
case models.IntervalAny:
|
||||
from = time.Time{}
|
||||
default:
|
||||
err = errors.New("invalid interval")
|
||||
}
|
||||
|
||||
return err, from, to
|
||||
}
|
128
main.go
128
main.go
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"github.com/lpar/gzipped/v2"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
@ -10,37 +11,42 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/lpar/gzipped/v2"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/routes/relay"
|
||||
"github.com/muety/wakapi/static/docs"
|
||||
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
httpSwagger "github.com/swaggo/http-swagger"
|
||||
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
"github.com/muety/wakapi/migrations"
|
||||
"github.com/muety/wakapi/repositories"
|
||||
"github.com/muety/wakapi/routes/api"
|
||||
"github.com/muety/wakapi/services/mail"
|
||||
fsutils "github.com/muety/wakapi/utils/fs"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
"github.com/muety/wakapi/routes"
|
||||
"github.com/muety/wakapi/routes/api"
|
||||
shieldsV1Routes "github.com/muety/wakapi/routes/compat/shields/v1"
|
||||
wtV1Routes "github.com/muety/wakapi/routes/compat/wakatime/v1"
|
||||
"github.com/muety/wakapi/routes/relay"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/services/mail"
|
||||
fsutils "github.com/muety/wakapi/utils/fs"
|
||||
|
||||
_ "gorm.io/driver/mysql"
|
||||
_ "gorm.io/driver/postgres"
|
||||
_ "gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
_ "github.com/muety/wakapi/static/docs"
|
||||
)
|
||||
|
||||
// Embed version.txt
|
||||
//
|
||||
//go:embed version.txt
|
||||
var version string
|
||||
|
||||
// Embed static files
|
||||
//
|
||||
//go:embed static
|
||||
var staticFiles embed.FS
|
||||
|
||||
@ -56,8 +62,10 @@ var (
|
||||
languageMappingRepository repositories.ILanguageMappingRepository
|
||||
projectLabelRepository repositories.IProjectLabelRepository
|
||||
summaryRepository repositories.ISummaryRepository
|
||||
leaderboardRepository *repositories.LeaderboardRepository
|
||||
keyValueRepository repositories.IKeyValueRepository
|
||||
diagnosticsRepository repositories.IDiagnosticsRepository
|
||||
metricsRepository *repositories.MetricsRepository
|
||||
)
|
||||
|
||||
var (
|
||||
@ -68,11 +76,13 @@ var (
|
||||
projectLabelService services.IProjectLabelService
|
||||
durationService services.IDurationService
|
||||
summaryService services.ISummaryService
|
||||
leaderboardService services.ILeaderboardService
|
||||
aggregationService services.IAggregationService
|
||||
mailService services.IMailService
|
||||
keyValueService services.IKeyValueService
|
||||
reportService services.IReportService
|
||||
diagnosticsService services.IDiagnosticsService
|
||||
housekeepingService services.IHousekeepingService
|
||||
miscService services.IMiscService
|
||||
)
|
||||
|
||||
@ -97,10 +107,12 @@ var (
|
||||
// @in header
|
||||
// @name Authorization
|
||||
|
||||
// @BasePath /api
|
||||
func main() {
|
||||
config = conf.Load(version)
|
||||
|
||||
// Configure Swagger docs
|
||||
docs.SwaggerInfo.BasePath = config.Server.BasePath + "/api"
|
||||
|
||||
// Set log level
|
||||
if config.IsDev() {
|
||||
logbuch.SetLevel(logbuch.LevelDebug)
|
||||
@ -120,9 +132,11 @@ func main() {
|
||||
|
||||
// Connect to database
|
||||
var err error
|
||||
db, err = gorm.Open(config.Db.GetDialector(), &gorm.Config{Logger: gormLogger})
|
||||
if config.Db.IsSQLite() {
|
||||
db.Exec("PRAGMA foreign_keys = ON;")
|
||||
logbuch.Info("starting with %s database", config.Db.Dialect)
|
||||
db, err = gorm.Open(config.Db.GetDialector(), &gorm.Config{Logger: gormLogger}, conf.GetWakapiDBOpts(&config.Db))
|
||||
if err != nil {
|
||||
logbuch.Error(err.Error())
|
||||
logbuch.Fatal("could not open database")
|
||||
}
|
||||
|
||||
if config.IsDev() {
|
||||
@ -138,7 +152,9 @@ func main() {
|
||||
defer sqlDb.Close()
|
||||
|
||||
// Migrate database schema
|
||||
migrations.Run(db, config)
|
||||
if !config.SkipMigrations {
|
||||
migrations.Run(db, config)
|
||||
}
|
||||
|
||||
// Repositories
|
||||
aliasRepository = repositories.NewAliasRepository(db)
|
||||
@ -147,8 +163,10 @@ func main() {
|
||||
languageMappingRepository = repositories.NewLanguageMappingRepository(db)
|
||||
projectLabelRepository = repositories.NewProjectLabelRepository(db)
|
||||
summaryRepository = repositories.NewSummaryRepository(db)
|
||||
leaderboardRepository = repositories.NewLeaderboardRepository(db)
|
||||
keyValueRepository = repositories.NewKeyValueRepository(db)
|
||||
diagnosticsRepository = repositories.NewDiagnosticsRepository(db)
|
||||
metricsRepository = repositories.NewMetricsRepository(db)
|
||||
|
||||
// Services
|
||||
mailService = mail.NewMailService()
|
||||
@ -159,18 +177,20 @@ func main() {
|
||||
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
|
||||
durationService = services.NewDurationService(heartbeatService)
|
||||
summaryService = services.NewSummaryService(summaryRepository, durationService, aliasService, projectLabelService)
|
||||
leaderboardService = services.NewLeaderboardService(leaderboardRepository, summaryService, userService)
|
||||
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
|
||||
keyValueService = services.NewKeyValueService(keyValueRepository)
|
||||
reportService = services.NewReportService(summaryService, userService, mailService)
|
||||
diagnosticsService = services.NewDiagnosticsService(diagnosticsRepository)
|
||||
miscService = services.NewMiscService(userService, summaryService, keyValueService)
|
||||
housekeepingService = services.NewHousekeepingService(userService, heartbeatService, summaryService)
|
||||
miscService = services.NewMiscService(userService, heartbeatService, summaryService, keyValueService, mailService)
|
||||
|
||||
// Schedule background tasks
|
||||
if !config.QuickStart {
|
||||
go aggregationService.Schedule()
|
||||
go miscService.ScheduleCountTotalTime()
|
||||
go reportService.Schedule()
|
||||
}
|
||||
go aggregationService.Schedule()
|
||||
go leaderboardService.Schedule()
|
||||
go reportService.Schedule()
|
||||
go housekeepingService.Schedule()
|
||||
go miscService.Schedule()
|
||||
|
||||
routes.Init()
|
||||
|
||||
@ -178,9 +198,10 @@ func main() {
|
||||
healthApiHandler := api.NewHealthApiHandler(db)
|
||||
heartbeatApiHandler := api.NewHeartbeatApiHandler(userService, heartbeatService, languageMappingService)
|
||||
summaryApiHandler := api.NewSummaryApiHandler(userService, summaryService)
|
||||
metricsHandler := api.NewMetricsHandler(userService, summaryService, heartbeatService, keyValueService)
|
||||
metricsHandler := api.NewMetricsHandler(userService, summaryService, heartbeatService, keyValueService, metricsRepository)
|
||||
diagnosticsHandler := api.NewDiagnosticsApiHandler(userService, diagnosticsService)
|
||||
avatarHandler := api.NewAvatarHandler()
|
||||
badgeHandler := api.NewBadgeHandler(userService, summaryService)
|
||||
|
||||
// Compat Handlers
|
||||
wakatimeV1StatusBarHandler := wtV1Routes.NewStatusBarHandler(userService, summaryService)
|
||||
@ -193,8 +214,10 @@ func main() {
|
||||
shieldV1BadgeHandler := shieldsV1Routes.NewBadgeHandler(summaryService, userService)
|
||||
|
||||
// MVC Handlers
|
||||
summaryHandler := routes.NewSummaryHandler(summaryService, userService)
|
||||
summaryHandler := routes.NewSummaryHandler(summaryService, userService, keyValueService)
|
||||
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, projectLabelService, keyValueService, mailService)
|
||||
subscriptionHandler := routes.NewSubscriptionHandler(userService, mailService, keyValueService)
|
||||
leaderboardHandler := routes.NewLeaderboardHandler(userService, leaderboardService)
|
||||
homeHandler := routes.NewHomeHandler(keyValueService)
|
||||
loginHandler := routes.NewLoginHandler(userService, mailService)
|
||||
imprintHandler := routes.NewImprintHandler(keyValueService)
|
||||
@ -229,7 +252,9 @@ func main() {
|
||||
loginHandler.RegisterRoutes(rootRouter)
|
||||
imprintHandler.RegisterRoutes(rootRouter)
|
||||
summaryHandler.RegisterRoutes(rootRouter)
|
||||
leaderboardHandler.RegisterRoutes(rootRouter)
|
||||
settingsHandler.RegisterRoutes(rootRouter)
|
||||
subscriptionHandler.RegisterRoutes(rootRouter)
|
||||
relayHandler.RegisterRoutes(rootRouter)
|
||||
|
||||
// API route registrations
|
||||
@ -239,6 +264,7 @@ func main() {
|
||||
metricsHandler.RegisterRoutes(apiRouter)
|
||||
diagnosticsHandler.RegisterRoutes(apiRouter)
|
||||
avatarHandler.RegisterRoutes(apiRouter)
|
||||
badgeHandler.RegisterRoutes(apiRouter)
|
||||
wakatimeV1StatusBarHandler.RegisterRoutes(apiRouter)
|
||||
wakatimeV1AllHandler.RegisterRoutes(apiRouter)
|
||||
wakatimeV1SummariesHandler.RegisterRoutes(apiRouter)
|
||||
@ -253,31 +279,17 @@ func main() {
|
||||
embeddedStatic, _ := fs.Sub(staticFiles, "static")
|
||||
static := conf.ChooseFS("static", embeddedStatic)
|
||||
|
||||
assetsFileServer := gzipped.FileServer(fsutils.NewExistsHttpFS(
|
||||
fsutils.NewExistsFS(static).WithCache(!config.IsDev()),
|
||||
))
|
||||
staticFileServer := http.FileServer(http.FS(
|
||||
fsutils.NeuteredFileSystem{FS: static},
|
||||
))
|
||||
assetsStaticFs := fsutils.NewExistsHttpFS(fsutils.NewExistsFS(static).WithCache(!config.IsDev()))
|
||||
assetsFileServer := http.FileServer(assetsStaticFs)
|
||||
if !config.IsDev() {
|
||||
assetsFileServer = gzipped.FileServer(assetsStaticFs)
|
||||
}
|
||||
staticFileServer := http.FileServer(http.FS(fsutils.NeuteredFileSystem{FS: static}))
|
||||
|
||||
router.PathPrefix("/contribute.json").Handler(staticFileServer)
|
||||
router.PathPrefix("/assets").Handler(assetsFileServer)
|
||||
router.PathPrefix("/swagger-ui").Handler(staticFileServer)
|
||||
router.PathPrefix("/docs").Handler(
|
||||
middlewares.NewFileTypeFilterMiddleware([]string{".go"})(staticFileServer),
|
||||
)
|
||||
|
||||
// Miscellaneous
|
||||
// Pre-warm projects cache
|
||||
if !config.IsDev() {
|
||||
allUsers, err := userService.GetAll()
|
||||
if err == nil {
|
||||
logbuch.Info("pre-warming user project cache")
|
||||
for _, u := range allUsers {
|
||||
go heartbeatService.GetEntitySetByUser(models.SummaryProject, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
router.Path("/swagger-ui").Handler(http.RedirectHandler("swagger-ui/", http.StatusMovedPermanently)) // https://github.com/swaggo/http-swagger/issues/44
|
||||
router.PathPrefix("/swagger-ui").Handler(httpSwagger.WrapHandler)
|
||||
|
||||
// Listen HTTP
|
||||
listen(router)
|
||||
@ -287,7 +299,7 @@ func listen(handler http.Handler) {
|
||||
var s4, s6, sSocket *http.Server
|
||||
|
||||
// IPv4
|
||||
if config.Server.ListenIpV4 != "" {
|
||||
if config.Server.ListenIpV4 != "-" && config.Server.ListenIpV4 != "" {
|
||||
bindString4 := config.Server.ListenIpV4 + ":" + strconv.Itoa(config.Server.Port)
|
||||
s4 = &http.Server{
|
||||
Handler: handler,
|
||||
@ -298,7 +310,7 @@ func listen(handler http.Handler) {
|
||||
}
|
||||
|
||||
// IPv6
|
||||
if config.Server.ListenIpV6 != "" {
|
||||
if config.Server.ListenIpV6 != "-" && config.Server.ListenIpV6 != "" {
|
||||
bindString6 := "[" + config.Server.ListenIpV6 + "]:" + strconv.Itoa(config.Server.Port)
|
||||
s6 = &http.Server{
|
||||
Handler: handler,
|
||||
@ -309,10 +321,10 @@ func listen(handler http.Handler) {
|
||||
}
|
||||
|
||||
// UNIX domain socket
|
||||
if config.Server.ListenSocket != "" {
|
||||
if config.Server.ListenSocket != "-" && config.Server.ListenSocket != "" {
|
||||
// Remove if exists
|
||||
if _, err := os.Stat(config.Server.ListenSocket); err == nil {
|
||||
logbuch.Info("--> Removing unix socket %s", config.Server.ListenSocket)
|
||||
logbuch.Info("👉 Removing unix socket %s", config.Server.ListenSocket)
|
||||
if err := os.Remove(config.Server.ListenSocket); err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
}
|
||||
@ -326,7 +338,7 @@ func listen(handler http.Handler) {
|
||||
|
||||
if config.UseTLS() {
|
||||
if s4 != nil {
|
||||
logbuch.Info("--> Listening for HTTPS on %s... ✅", s4.Addr)
|
||||
logbuch.Info("👉 Listening for HTTPS on %s... ✅", s4.Addr)
|
||||
go func() {
|
||||
if err := s4.ListenAndServeTLS(config.Server.TlsCertPath, config.Server.TlsKeyPath); err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
@ -334,7 +346,7 @@ func listen(handler http.Handler) {
|
||||
}()
|
||||
}
|
||||
if s6 != nil {
|
||||
logbuch.Info("--> Listening for HTTPS on %s... ✅", s6.Addr)
|
||||
logbuch.Info("👉 Listening for HTTPS on %s... ✅", s6.Addr)
|
||||
go func() {
|
||||
if err := s6.ListenAndServeTLS(config.Server.TlsCertPath, config.Server.TlsKeyPath); err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
@ -342,12 +354,15 @@ func listen(handler http.Handler) {
|
||||
}()
|
||||
}
|
||||
if sSocket != nil {
|
||||
logbuch.Info("--> Listening for HTTPS on %s... ✅", config.Server.ListenSocket)
|
||||
logbuch.Info("👉 Listening for HTTPS on %s... ✅", config.Server.ListenSocket)
|
||||
go func() {
|
||||
unixListener, err := net.Listen("unix", config.Server.ListenSocket)
|
||||
if err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
}
|
||||
if err := os.Chmod(config.Server.ListenSocket, os.FileMode(config.Server.ListenSocketMode)); err != nil {
|
||||
logbuch.Warn("failed to set user permissions for unix socket, %v", err)
|
||||
}
|
||||
if err := sSocket.ServeTLS(unixListener, config.Server.TlsCertPath, config.Server.TlsKeyPath); err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
}
|
||||
@ -355,7 +370,7 @@ func listen(handler http.Handler) {
|
||||
}
|
||||
} else {
|
||||
if s4 != nil {
|
||||
logbuch.Info("--> Listening for HTTP on %s... ✅", s4.Addr)
|
||||
logbuch.Info("👉 Listening for HTTP on %s... ✅", s4.Addr)
|
||||
go func() {
|
||||
if err := s4.ListenAndServe(); err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
@ -363,7 +378,7 @@ func listen(handler http.Handler) {
|
||||
}()
|
||||
}
|
||||
if s6 != nil {
|
||||
logbuch.Info("--> Listening for HTTP on %s... ✅", s6.Addr)
|
||||
logbuch.Info("👉 Listening for HTTP on %s... ✅", s6.Addr)
|
||||
go func() {
|
||||
if err := s6.ListenAndServe(); err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
@ -371,12 +386,15 @@ func listen(handler http.Handler) {
|
||||
}()
|
||||
}
|
||||
if sSocket != nil {
|
||||
logbuch.Info("--> Listening for HTTP on %s... ✅", config.Server.ListenSocket)
|
||||
logbuch.Info("👉 Listening for HTTP on %s... ✅", config.Server.ListenSocket)
|
||||
go func() {
|
||||
unixListener, err := net.Listen("unix", config.Server.ListenSocket)
|
||||
if err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
}
|
||||
if err := os.Chmod(config.Server.ListenSocket, os.FileMode(config.Server.ListenSocketMode)); err != nil {
|
||||
logbuch.Warn("failed to set user permissions for unix socket, %v", err)
|
||||
}
|
||||
if err := sSocket.Serve(unixListener); err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package middlewares
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/muety/wakapi/helpers"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@ -21,10 +22,11 @@ var (
|
||||
)
|
||||
|
||||
type AuthenticateMiddleware struct {
|
||||
config *conf.Config
|
||||
userSrvc services.IUserService
|
||||
optionalForPaths []string
|
||||
redirectTarget string // optional
|
||||
config *conf.Config
|
||||
userSrvc services.IUserService
|
||||
optionalForPaths []string
|
||||
redirectTarget string // optional
|
||||
redirectErrorMessage string // optional
|
||||
}
|
||||
|
||||
func NewAuthenticateMiddleware(userService services.IUserService) *AuthenticateMiddleware {
|
||||
@ -45,6 +47,11 @@ func (m *AuthenticateMiddleware) WithRedirectTarget(path string) *AuthenticateMi
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *AuthenticateMiddleware) WithRedirectErrorMessage(message string) *AuthenticateMiddleware {
|
||||
m.redirectErrorMessage = message
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *AuthenticateMiddleware) Handler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
m.ServeHTTP(w, r, h.ServeHTTP)
|
||||
@ -72,6 +79,11 @@ func (m *AuthenticateMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reques
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(conf.ErrUnauthorized))
|
||||
} else {
|
||||
if m.redirectErrorMessage != "" {
|
||||
session, _ := conf.GetSessionStore().Get(r, conf.SessionKeyDefault)
|
||||
session.AddFlash(m.redirectErrorMessage, "error")
|
||||
session.Save(r, w)
|
||||
}
|
||||
http.SetCookie(w, m.config.GetClearCookie(models.AuthCookieKey))
|
||||
http.Redirect(w, r, m.redirectTarget, http.StatusFound)
|
||||
}
|
||||
@ -121,7 +133,7 @@ func (m *AuthenticateMiddleware) tryGetUserByApiKeyQuery(r *http.Request) (*mode
|
||||
}
|
||||
|
||||
func (m *AuthenticateMiddleware) tryGetUserByCookie(r *http.Request) (*models.User, error) {
|
||||
username, err := utils.ExtractCookieAuth(r, m.config)
|
||||
username, err := helpers.ExtractCookieAuth(r, m.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -160,17 +160,23 @@ func (m *WakatimeRelayMiddleware) filterByCache(r *http.Request) error {
|
||||
|
||||
newData := make([]interface{}, 0, len(heartbeats))
|
||||
|
||||
for i, hb := range heartbeats {
|
||||
hb = hb.Hashed()
|
||||
|
||||
process := func(heartbeat *models.Heartbeat, rawData interface{}) {
|
||||
heartbeat = heartbeat.Hashed()
|
||||
// we didn't see this particular heartbeat before
|
||||
if _, found := m.hashCache.Get(hb.Hash); !found {
|
||||
m.hashCache.SetDefault(hb.Hash, true)
|
||||
newData = append(newData, rawData.([]interface{})[i])
|
||||
continue
|
||||
if _, found := m.hashCache.Get(heartbeat.Hash); !found {
|
||||
m.hashCache.SetDefault(heartbeat.Hash, true)
|
||||
newData = append(newData, rawData)
|
||||
}
|
||||
}
|
||||
|
||||
if _, isList := rawData.([]interface{}); isList {
|
||||
for i, hb := range heartbeats {
|
||||
process(hb, rawData.([]interface{})[i])
|
||||
}
|
||||
} else if len(heartbeats) > 0 {
|
||||
process(heartbeats[0], rawData.(interface{}))
|
||||
}
|
||||
|
||||
if len(newData) == 0 {
|
||||
return errors.New("no new heartbeats to relay")
|
||||
}
|
||||
|
@ -20,6 +20,13 @@ func (c *PrincipalContainer) GetPrincipal() *models.User {
|
||||
return c.principal
|
||||
}
|
||||
|
||||
func (c *PrincipalContainer) GetPrincipalIdentity() string {
|
||||
if c.principal == nil {
|
||||
return ""
|
||||
}
|
||||
return c.principal.Identity()
|
||||
}
|
||||
|
||||
// This middleware is a bit of a dirty workaround to the fact that a http.Request's context
|
||||
// does not allow to pass values from an inner to an outer middleware. Calling WithContext() on a
|
||||
// request shallow-copies the whole request itself and therefore, in a chain of handler1(handler2()),
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
|
||||
var securityHeaders = map[string]string{
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Content-Security-Policy": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' https: data:; form-action 'self'; block-all-mixed-content;",
|
||||
"Content-Security-Policy": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' https: data:; form-action 'self' *.stripe.com; block-all-mixed-content;",
|
||||
"X-Frame-Options": "DENY",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
}
|
||||
|
56
migrations/20220317_align_num_heartbeats.go
Normal file
56
migrations/20220317_align_num_heartbeats.go
Normal file
@ -0,0 +1,56 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
const name = "20220317-align_num_heartbeats"
|
||||
f := migrationFunc{
|
||||
name: name,
|
||||
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||
if hasRun(name, db) {
|
||||
return nil
|
||||
}
|
||||
|
||||
logbuch.Info("this may take a while!")
|
||||
|
||||
// find all summaries whose num_heartbeats is zero even though they have items
|
||||
var faultyIds []uint
|
||||
|
||||
if err := db.Model(&models.Summary{}).
|
||||
Distinct("summaries.id").
|
||||
Joins("INNER JOIN summary_items ON summaries.num_heartbeats = 0 AND summaries.id = summary_items.summary_id").
|
||||
Scan(&faultyIds).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update their heartbeats counter
|
||||
result := db.
|
||||
Table("summaries AS s1").
|
||||
Where("s1.id IN ?", faultyIds).
|
||||
Update(
|
||||
"num_heartbeats",
|
||||
db.
|
||||
Model(&models.Heartbeat{}).
|
||||
Select("COUNT(*)").
|
||||
Where("user_id = ?", gorm.Expr("s1.user_id")).
|
||||
Where("time BETWEEN ? AND ?", gorm.Expr("s1.from_time"), gorm.Expr("s1.to_time")),
|
||||
)
|
||||
|
||||
if err := result.Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logbuch.Info("corrected heartbeats counter of %d summaries", result.RowsAffected)
|
||||
|
||||
setHasRun(name, db)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
registerPostMigration(f)
|
||||
}
|
41
migrations/20220318_mysql_timestamp_precision.go
Normal file
41
migrations/20220318_mysql_timestamp_precision.go
Normal file
@ -0,0 +1,41 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/muety/wakapi/config"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
const name = "20220318-mysql_timestamp_precision"
|
||||
f := migrationFunc{
|
||||
name: name,
|
||||
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||
if hasRun(name, db) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if cfg.Db.IsMySQL() {
|
||||
logbuch.Info("altering heartbeats table, this may take a while (up to hours)")
|
||||
|
||||
db.Exec("SET foreign_key_checks=0;")
|
||||
db.Exec("SET unique_checks=0;")
|
||||
if err := db.Exec("ALTER TABLE heartbeats MODIFY COLUMN `time` TIMESTAMP(3) NOT NULL").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Exec("ALTER TABLE heartbeats MODIFY COLUMN `created_at` TIMESTAMP(3) NOT NULL").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
db.Exec("SET foreign_key_checks=1;")
|
||||
db.Exec("SET unique_checks=1;")
|
||||
|
||||
logbuch.Info("migrated timestamp columns to millisecond precision")
|
||||
}
|
||||
|
||||
setHasRun(name, db)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
registerPostMigration(f)
|
||||
}
|
39
migrations/202203191_drop_diagnostics_user.go
Normal file
39
migrations/202203191_drop_diagnostics_user.go
Normal file
@ -0,0 +1,39 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
const name = "202203191-drop_diagnostics_user"
|
||||
f := migrationFunc{
|
||||
name: name,
|
||||
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||
if hasRun(name, db) {
|
||||
return nil
|
||||
}
|
||||
|
||||
migrator := db.Migrator()
|
||||
|
||||
if migrator.HasColumn(&models.Diagnostics{}, "user_id") {
|
||||
logbuch.Info("running migration '%s'", name)
|
||||
|
||||
if err := migrator.DropConstraint(&models.Diagnostics{}, "fk_diagnostics_user"); err != nil {
|
||||
logbuch.Warn("failed to drop 'fk_diagnostics_user' constraint (%v)", err)
|
||||
}
|
||||
|
||||
if err := migrator.DropColumn(&models.Diagnostics{}, "user_id"); err != nil {
|
||||
logbuch.Warn("failed to drop user_id column of diagnostics (%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
setHasRun(name, db)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
registerPostMigration(f)
|
||||
}
|
40
migrations/20220403_drop_user_project_idx.go
Normal file
40
migrations/20220403_drop_user_project_idx.go
Normal file
@ -0,0 +1,40 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// migration to fix https://github.com/muety/wakapi/issues/346
|
||||
// caused by https://github.com/muety/wakapi/blob/2.3.2/migrations/20220319_add_user_project_idx.go in combination with
|
||||
// the wrongly defined index at https://github.com/muety/wakapi/blob/5aae18e2415d9e620f383f98cd8cbdf39cd99f27/models/heartbeat.go#L18
|
||||
// and https://github.com/go-gorm/sqlite/issues/87
|
||||
// -> drop index and let it be auto-created again with properly formatted ddl
|
||||
|
||||
func init() {
|
||||
const name = "20220403-drop_user_project_idx"
|
||||
const idxName = "idx_user_project"
|
||||
|
||||
f := migrationFunc{
|
||||
name: name,
|
||||
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||
if !db.Migrator().HasTable(&models.KeyStringValue{}) || hasRun(name, db) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if cfg.Db.IsSQLite() && db.Migrator().HasIndex(&models.Heartbeat{}, idxName) {
|
||||
logbuch.Info("running migration '%s'", name)
|
||||
if err := db.Migrator().DropIndex(&models.Heartbeat{}, idxName); err != nil {
|
||||
logbuch.Warn("failed to drop %s", idxName)
|
||||
}
|
||||
}
|
||||
|
||||
setHasRun(name, db)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
registerPreMigration(f)
|
||||
}
|
24
migrations/20220930_drop_heartbeats_entity_idx.go
Normal file
24
migrations/20220930_drop_heartbeats_entity_idx.go
Normal file
@ -0,0 +1,24 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
const name = "20220930-drop_heartbeats_entity_idx"
|
||||
const idxName = "idx_entity"
|
||||
|
||||
f := migrationFunc{
|
||||
name: name,
|
||||
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||
if !db.Migrator().HasTable(&models.Heartbeat{}) || !db.Migrator().HasIndex(&models.Heartbeat{}, idxName) {
|
||||
return nil
|
||||
}
|
||||
return db.Migrator().DropIndex(&models.Heartbeat{}, idxName)
|
||||
},
|
||||
}
|
||||
|
||||
registerPreMigration(f)
|
||||
}
|
88
migrations/20221002_fix_summary_id_types.go
Normal file
88
migrations/20221002_fix_summary_id_types.go
Normal file
@ -0,0 +1,88 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// fix for https://github.com/muety/wakapi/issues/416
|
||||
|
||||
func init() {
|
||||
const name = "20221002-fix_summary_id_types"
|
||||
|
||||
f := migrationFunc{
|
||||
name: name,
|
||||
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||
if cfg.Db.Dialect != config.SQLDialectMysql {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !db.Migrator().HasTable(&models.Summary{}) || !db.Migrator().HasTable(&models.SummaryItem{}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var currentType string
|
||||
if err := db.
|
||||
Table("information_schema.columns").
|
||||
Select("data_type").
|
||||
Where("table_name = ?", "summary_items").
|
||||
Where("column_name = ?", "summary_id").
|
||||
Limit(1).
|
||||
Row().Scan(¤tType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.ToLower(currentType) != "int" {
|
||||
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_editors") {
|
||||
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_editors"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_languages") {
|
||||
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_languages"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_machines") {
|
||||
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_machines"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_operating_systems") {
|
||||
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_operating_systems"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_projects") {
|
||||
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_projects"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// https://github.com/muety/wakapi/issues/416#issuecomment-1271674792
|
||||
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summary_items_summary") {
|
||||
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summary_items_summary"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_labels") {
|
||||
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_labels"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Migrator().AlterColumn(&models.Summary{}, "id"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Migrator().AlterColumn(&models.SummaryItem{}, "summary_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
registerPreMigration(f)
|
||||
}
|
35
migrations/20221016_drop_rank_column.go
Normal file
35
migrations/20221016_drop_rank_column.go
Normal file
@ -0,0 +1,35 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
const name = "20221016-drop_rank_column"
|
||||
f := migrationFunc{
|
||||
name: name,
|
||||
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||
if hasRun(name, db) {
|
||||
return nil
|
||||
}
|
||||
|
||||
migrator := db.Migrator()
|
||||
|
||||
if migrator.HasTable(&models.LeaderboardItem{}) && migrator.HasColumn(&models.LeaderboardItem{}, "rank") {
|
||||
logbuch.Info("running migration '%s'", name)
|
||||
|
||||
if err := migrator.DropColumn(&models.LeaderboardItem{}, "rank"); err != nil {
|
||||
logbuch.Warn("failed to drop 'rank' column (%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
setHasRun(name, db)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
registerPostMigration(f)
|
||||
}
|
71
migrations/20221028_fix_heartbeats_time_user_idx.go
Normal file
71
migrations/20221028_fix_heartbeats_time_user_idx.go
Normal file
@ -0,0 +1,71 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// due to an error in the model definition, idx_time_user used to only cover 'user_id', but not time column
|
||||
// if that's the case in the current state of the database, drop the index and let it be recreated by auto migration afterwards
|
||||
func init() {
|
||||
const name = "20221028-fix_heartbeats_time_user_idx"
|
||||
f := migrationFunc{
|
||||
name: name,
|
||||
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||
migrator := db.Migrator()
|
||||
|
||||
if !migrator.HasTable(&models.Heartbeat{}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var drop bool
|
||||
if cfg.Db.IsSQLite() {
|
||||
// sqlite migrator doesn't support GetIndexes() currently
|
||||
var ddl string
|
||||
if err := db.
|
||||
Table("sqlite_schema").
|
||||
Select("sql").
|
||||
Where("type = 'index'").
|
||||
Where("tbl_name = 'heartbeats'").
|
||||
Where("name = 'idx_time_user'").
|
||||
Scan(&ddl).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
matches := regexp.MustCompile("(?i)\\((.+)\\)$").FindStringSubmatch(ddl)
|
||||
if len(matches) > 0 && !strings.Contains(matches[0], "`user_id`") || !strings.Contains(matches[0], "`time`") {
|
||||
drop = true
|
||||
}
|
||||
} else {
|
||||
indexes, err := migrator.GetIndexes(&models.Heartbeat{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, idx := range indexes {
|
||||
if idx.Table() == "heartbeats" && idx.Name() == "idx_time_user" && len(idx.Columns()) == 1 {
|
||||
drop = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !drop {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := migrator.DropIndex(&models.Heartbeat{}, "idx_time_user"); err != nil {
|
||||
return err
|
||||
}
|
||||
logbuch.Info("index 'idx_time_user' needs to be recreated, this may take a while")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
registerPreMigration(f)
|
||||
}
|
@ -3,11 +3,14 @@ package migrations
|
||||
import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type gormMigrationFunc func(db *gorm.DB) error
|
||||
|
||||
type migrationFunc struct {
|
||||
f func(db *gorm.DB, cfg *config.Config) error
|
||||
name string
|
||||
@ -20,6 +23,45 @@ var (
|
||||
postMigrations migrationFuncs
|
||||
)
|
||||
|
||||
func GetMigrationFunc(cfg *config.Config) gormMigrationFunc {
|
||||
switch cfg.Db.Dialect {
|
||||
default:
|
||||
return func(db *gorm.DB) error {
|
||||
if err := db.AutoMigrate(&models.User{}); err != nil && !cfg.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.KeyStringValue{}); err != nil && !cfg.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.Alias{}); err != nil && !cfg.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.Heartbeat{}); err != nil && !cfg.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.Summary{}); err != nil && !cfg.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.SummaryItem{}); err != nil && !cfg.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.LanguageMapping{}); err != nil && !cfg.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.ProjectLabel{}); err != nil && !cfg.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.Diagnostics{}); err != nil && !cfg.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.LeaderboardItem{}); err != nil && !cfg.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func registerPreMigration(f migrationFunc) {
|
||||
preMigrations = append(preMigrations, f)
|
||||
}
|
||||
@ -35,7 +77,7 @@ func Run(db *gorm.DB, cfg *config.Config) {
|
||||
}
|
||||
|
||||
func RunSchemaMigrations(db *gorm.DB, cfg *config.Config) {
|
||||
if err := cfg.GetMigrationFunc(cfg.Db.Dialect)(db); err != nil {
|
||||
if err := GetMigrationFunc(cfg)(db); err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
@ -10,18 +10,18 @@ type HeartbeatServiceMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *HeartbeatServiceMock) Insert(heartbeat *models.Heartbeat) error {
|
||||
args := m.Called(heartbeat)
|
||||
func (m *HeartbeatServiceMock) Insert(h *models.Heartbeat) error {
|
||||
args := m.Called(h)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *HeartbeatServiceMock) InsertBatch(heartbeats []*models.Heartbeat) error {
|
||||
args := m.Called(heartbeats)
|
||||
func (m *HeartbeatServiceMock) InsertBatch(h []*models.Heartbeat) error {
|
||||
args := m.Called(h)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *HeartbeatServiceMock) Count() (int64, error) {
|
||||
args := m.Called()
|
||||
func (m *HeartbeatServiceMock) Count(a bool) (int64, error) {
|
||||
args := m.Called(a)
|
||||
return int64(args.Int(0)), args.Error(1)
|
||||
}
|
||||
|
||||
@ -40,6 +40,11 @@ func (m *HeartbeatServiceMock) GetAllWithin(time time.Time, time2 time.Time, use
|
||||
return args.Get(0).([]*models.Heartbeat), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *HeartbeatServiceMock) GetAllWithinByFilters(time time.Time, time2 time.Time, user *models.User, filters *models.Filters) ([]*models.Heartbeat, error) {
|
||||
args := m.Called(time, time2, user, filters)
|
||||
return args.Get(0).([]*models.Heartbeat), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *HeartbeatServiceMock) GetFirstByUsers() ([]*models.TimeByUser, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).([]*models.TimeByUser), args.Error(1)
|
||||
@ -64,3 +69,13 @@ func (m *HeartbeatServiceMock) DeleteBefore(time time.Time) error {
|
||||
args := m.Called(time)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *HeartbeatServiceMock) DeleteByUser(u *models.User) error {
|
||||
args := m.Called(u)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *HeartbeatServiceMock) DeleteByUserBefore(u *models.User, t time.Time) error {
|
||||
args := m.Called(u, t)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
@ -10,8 +10,8 @@ type SummaryRepositoryMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *SummaryRepositoryMock) Insert(summary *models.Summary) error {
|
||||
args := m.Called(summary)
|
||||
func (m *SummaryRepositoryMock) Insert(s *models.Summary) error {
|
||||
args := m.Called(s)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
@ -20,8 +20,8 @@ func (m *SummaryRepositoryMock) GetAll() ([]*models.Summary, error) {
|
||||
return args.Get(0).([]*models.Summary), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *SummaryRepositoryMock) GetByUserWithin(user *models.User, time time.Time, time2 time.Time) ([]*models.Summary, error) {
|
||||
args := m.Called(user, time, time2)
|
||||
func (m *SummaryRepositoryMock) GetByUserWithin(u *models.User, t1 time.Time, t2 time.Time) ([]*models.Summary, error) {
|
||||
args := m.Called(u, t1, t2)
|
||||
return args.Get(0).([]*models.Summary), args.Error(1)
|
||||
}
|
||||
|
||||
@ -34,3 +34,8 @@ func (m *SummaryRepositoryMock) DeleteByUser(s string) error {
|
||||
args := m.Called(s)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *SummaryRepositoryMock) DeleteByUserBefore(s string, t time.Time) error {
|
||||
args := m.Called(s, t)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
@ -34,11 +34,31 @@ func (m *UserServiceMock) GetAll() ([]*models.User, error) {
|
||||
return args.Get(0).([]*models.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *UserServiceMock) GetMany(s []string) ([]*models.User, error) {
|
||||
args := m.Called(s)
|
||||
return args.Get(0).([]*models.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *UserServiceMock) GetManyMapped(s []string) (map[string]*models.User, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).(map[string]*models.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *UserServiceMock) GetAllByLeaderboard(b bool) ([]*models.User, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m *UserServiceMock) GetAllByReports(b bool) ([]*models.User, error) {
|
||||
args := m.Called(b)
|
||||
return args.Get(0).([]*models.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *UserServiceMock) GetUserByStripeCustomerId(s string) (*models.User, error) {
|
||||
args := m.Called(s)
|
||||
return args.Get(0).(*models.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *UserServiceMock) GetActive(b bool) ([]*models.User, error) {
|
||||
args := m.Called(b)
|
||||
return args.Get(0).([]*models.User), args.Error(1)
|
||||
@ -92,3 +112,7 @@ func (m *UserServiceMock) GenerateResetToken(user *models.User) (*models.User, e
|
||||
func (m *UserServiceMock) FlushCache() {
|
||||
m.Called()
|
||||
}
|
||||
|
||||
func (m *UserServiceMock) FlushUserCache(s string) {
|
||||
m.Called(s)
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/helpers"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/utils"
|
||||
)
|
||||
|
||||
// https://shields.io/endpoint
|
||||
|
||||
const (
|
||||
defaultLabel = "coding time"
|
||||
defaultColor = "#2D3748" // not working
|
||||
defaultLabel = "wakapi.dev"
|
||||
defaultColor = "2F855A"
|
||||
)
|
||||
|
||||
type BadgeData struct {
|
||||
@ -23,7 +23,7 @@ func NewBadgeDataFrom(summary *models.Summary) *BadgeData {
|
||||
return &BadgeData{
|
||||
SchemaVersion: 1,
|
||||
Label: defaultLabel,
|
||||
Message: utils.FmtWakatimeDuration(summary.TotalTime()),
|
||||
Message: helpers.FmtWakatimeDuration(summary.TotalTime()),
|
||||
Color: defaultColor,
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/helpers"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
// https://wakatime.com/developers#all_time_since_today
|
||||
@ -28,11 +29,19 @@ type AllTimeRange struct {
|
||||
|
||||
func NewAllTimeFrom(summary *models.Summary) *AllTimeViewModel {
|
||||
total := summary.TotalTime()
|
||||
tzName, _ := summary.FromTime.T().Zone()
|
||||
return &AllTimeViewModel{
|
||||
Data: &AllTimeData{
|
||||
TotalSeconds: float32(total.Seconds()),
|
||||
Text: utils.FmtWakatimeDuration(total),
|
||||
Text: helpers.FmtWakatimeDuration(total),
|
||||
IsUpToDate: true,
|
||||
Range: &AllTimeRange{
|
||||
End: summary.ToTime.T().Format(time.RFC3339),
|
||||
EndDate: helpers.FormatDate(summary.ToTime.T()),
|
||||
Start: summary.FromTime.T().Format(time.RFC3339),
|
||||
StartDate: helpers.FormatDate(summary.FromTime.T()),
|
||||
Timezone: tzName,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ package v1
|
||||
// https://wakatime.com/api/v1/users/current/machine_names
|
||||
|
||||
type MachineViewModel struct {
|
||||
Data []*MachineEntry `json:"data"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
Data []*MachineEntry `json:"data"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
type MachineEntry struct {
|
||||
|
@ -2,8 +2,8 @@ package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/muety/wakapi/helpers"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
@ -13,9 +13,17 @@ import (
|
||||
// https://pastr.de/v/736450
|
||||
|
||||
type SummariesViewModel struct {
|
||||
Data []*SummariesData `json:"data"`
|
||||
End time.Time `json:"end"`
|
||||
Start time.Time `json:"start"`
|
||||
Data []*SummariesData `json:"data"`
|
||||
End time.Time `json:"end"`
|
||||
Start time.Time `json:"start"`
|
||||
CumulativeTotal *SummariesCumulativeTotal `json:"cummulative_total"` // typo is intended
|
||||
}
|
||||
|
||||
type SummariesCumulativeTotal struct {
|
||||
Decimal string `json:"decimal"`
|
||||
Digital string `json:"digital"`
|
||||
Seconds float64 `json:"seconds"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type SummariesData struct {
|
||||
@ -73,10 +81,23 @@ func NewSummariesFrom(summaries []*models.Summary) *SummariesViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
var totalTime time.Duration
|
||||
for _, s := range summaries {
|
||||
totalTime += s.TotalTime()
|
||||
}
|
||||
|
||||
totalHrs, totalMins, totalSecs := totalTime.Hours(), (totalTime - time.Duration(totalTime.Hours())*time.Hour).Minutes(), totalTime.Seconds()
|
||||
|
||||
return &SummariesViewModel{
|
||||
Data: data,
|
||||
End: maxDate,
|
||||
Start: minDate,
|
||||
CumulativeTotal: &SummariesCumulativeTotal{
|
||||
Decimal: fmt.Sprintf("%.2f", totalHrs),
|
||||
Digital: fmt.Sprintf("%d:%d", int(totalHrs), int(totalMins)),
|
||||
Seconds: totalSecs,
|
||||
Text: helpers.FmtWakatimeDuration(totalTime),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,7 +119,7 @@ func newDataFrom(s *models.Summary) *SummariesData {
|
||||
Digital: fmt.Sprintf("%d:%d", totalHrs, totalMins),
|
||||
Hours: totalHrs,
|
||||
Minutes: totalMins,
|
||||
Text: utils.FmtWakatimeDuration(total),
|
||||
Text: helpers.FmtWakatimeDuration(total),
|
||||
TotalSeconds: total.Seconds(),
|
||||
},
|
||||
Range: &SummariesRange{
|
||||
@ -180,7 +201,7 @@ func convertEntry(e *models.SummaryItem, entityTotal time.Duration) *SummariesEn
|
||||
Name: e.Key,
|
||||
Percent: percentage,
|
||||
Seconds: secs,
|
||||
Text: utils.FmtWakatimeDuration(total),
|
||||
Text: helpers.FmtWakatimeDuration(total),
|
||||
TotalSeconds: total.Seconds(),
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package v1
|
||||
|
||||
type UserAgentsViewModel struct {
|
||||
Data []*UserAgentEntry `json:"data"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
Data []*UserAgentEntry `json:"data"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
type UserAgentEntry struct {
|
||||
|
@ -2,8 +2,6 @@ package models
|
||||
|
||||
type Diagnostics struct {
|
||||
ID uint `gorm:"primary_key"`
|
||||
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
UserID string `json:"-" gorm:"not null; index:idx_diagnostics_user"`
|
||||
Platform string `json:"platform"`
|
||||
Architecture string `json:"architecture"`
|
||||
Plugin string `json:"plugin"`
|
||||
|
@ -92,7 +92,7 @@ func (f *Filters) OneOrEmpty() FilterElement {
|
||||
if ok, t, of := f.One(); ok {
|
||||
return FilterElement{entity: t, filter: of}
|
||||
}
|
||||
return FilterElement{}
|
||||
return FilterElement{entity: SummaryUnknown, filter: []string{}}
|
||||
}
|
||||
|
||||
func (f *Filters) IsEmpty() bool {
|
||||
@ -100,6 +100,49 @@ func (f *Filters) IsEmpty() bool {
|
||||
return !nonEmpty
|
||||
}
|
||||
|
||||
func (f *Filters) Count() int {
|
||||
var count int
|
||||
for i := SummaryProject; i <= SummaryBranch; i++ {
|
||||
count += f.CountByEntity(i)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (f *Filters) CountByEntity(entity uint8) int {
|
||||
return len(*f.ResolveEntity(entity))
|
||||
}
|
||||
|
||||
func (f *Filters) EntityCount() int {
|
||||
var count int
|
||||
for i := SummaryProject; i <= SummaryBranch; i++ {
|
||||
if c := f.CountByEntity(i); c > 0 {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (f *Filters) ResolveEntity(entityId uint8) *OrFilter {
|
||||
switch entityId {
|
||||
case SummaryProject:
|
||||
return &f.Project
|
||||
case SummaryLanguage:
|
||||
return &f.Language
|
||||
case SummaryEditor:
|
||||
return &f.Editor
|
||||
case SummaryOS:
|
||||
return &f.OS
|
||||
case SummaryMachine:
|
||||
return &f.Machine
|
||||
case SummaryLabel:
|
||||
return &f.Label
|
||||
case SummaryBranch:
|
||||
return &f.Branch
|
||||
default:
|
||||
return &OrFilter{}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filters) Hash() string {
|
||||
hash, err := hashstructure.Hash(f, hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
|
@ -11,29 +11,34 @@ import (
|
||||
type Heartbeat struct {
|
||||
ID uint64 `gorm:"primary_key" hash:"ignore"`
|
||||
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" hash:"ignore"`
|
||||
UserID string `json:"-" gorm:"not null; index:idx_time_user"`
|
||||
Entity string `json:"entity" gorm:"not null; index:idx_entity"`
|
||||
Type string `json:"type"`
|
||||
Category string `json:"category"`
|
||||
Project string `json:"project"`
|
||||
Branch string `json:"branch"`
|
||||
UserID string `json:"-" gorm:"not null; index:idx_time_user; index:idx_user_project"` // idx_user_project is for quickly fetching a user's project list (settings page)
|
||||
Entity string `json:"entity" gorm:"not null"`
|
||||
Type string `json:"type" gorm:"size:255"`
|
||||
Category string `json:"category" gorm:"size:255"`
|
||||
Project string `json:"project" gorm:"index:idx_project; index:idx_user_project"`
|
||||
Branch string `json:"branch" gorm:"index:idx_branch"`
|
||||
Language string `json:"language" gorm:"index:idx_language"`
|
||||
IsWrite bool `json:"is_write"`
|
||||
Editor string `json:"editor" hash:"ignore"` // ignored because editor might be parsed differently by wakatime
|
||||
OperatingSystem string `json:"operating_system" hash:"ignore"` // ignored because os might be parsed differently by wakatime
|
||||
Machine string `json:"machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
|
||||
UserAgent string `json:"user_agent" hash:"ignore"`
|
||||
Time CustomTime `json:"time" gorm:"type:timestamp; index:idx_time,idx_time_user" swaggertype:"primitive,number"`
|
||||
Editor string `json:"editor" gorm:"index:idx_editor" hash:"ignore"` // ignored because editor might be parsed differently by wakatime
|
||||
OperatingSystem string `json:"operating_system" gorm:"index:idx_operating_system" hash:"ignore"` // ignored because os might be parsed differently by wakatime
|
||||
Machine string `json:"machine" gorm:"index:idx_machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
|
||||
UserAgent string `json:"user_agent" hash:"ignore" gorm:"type:varchar(255)"`
|
||||
Time CustomTime `json:"time" gorm:"type:timestamp(3); index:idx_time; index:idx_time_user" swaggertype:"primitive,number"`
|
||||
Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"`
|
||||
Origin string `json:"-" hash:"ignore"`
|
||||
OriginId string `json:"-" hash:"ignore"`
|
||||
CreatedAt CustomTime `json:"created_at" gorm:"type:timestamp" swaggertype:"primitive,number" hash:"ignore"` // https://gorm.io/docs/conventions.html#CreatedAt
|
||||
Origin string `json:"-" hash:"ignore" gorm:"type:varchar(255)"`
|
||||
OriginId string `json:"-" hash:"ignore" gorm:"type:varchar(255)"`
|
||||
CreatedAt CustomTime `json:"created_at" gorm:"type:timestamp(3)" swaggertype:"primitive,number" hash:"ignore"` // https://gorm.io/docs/conventions.html#CreatedAt
|
||||
}
|
||||
|
||||
func (h *Heartbeat) Valid() bool {
|
||||
return h.User != nil && h.UserID != "" && h.User.ID == h.UserID && h.Time != CustomTime(time.Time{})
|
||||
}
|
||||
|
||||
func (h *Heartbeat) Timely(maxAge time.Duration) bool {
|
||||
now := time.Now()
|
||||
return now.Sub(h.Time.T()) <= maxAge && h.Time.T().Sub(now) < 1*time.Hour
|
||||
}
|
||||
|
||||
func (h *Heartbeat) Augment(languageMappings map[string]string) {
|
||||
maxPrec := -1 // precision / mapping complexity -> more concrete ones shall take precedence
|
||||
for ending, value := range languageMappings {
|
||||
@ -99,3 +104,15 @@ func (h *Heartbeat) Hashed() *Heartbeat {
|
||||
h.Hash = fmt.Sprintf("%x", hash) // "uint64 values with high bit set are not supported"
|
||||
return h
|
||||
}
|
||||
|
||||
func GetEntityColumn(t uint8) string {
|
||||
return []string{
|
||||
"project",
|
||||
"language",
|
||||
"editor",
|
||||
"operating_system",
|
||||
"machine",
|
||||
"label",
|
||||
"branch",
|
||||
}[t]
|
||||
}
|
||||
|
@ -64,3 +64,42 @@ func TestHeartbeat_GetKey(t *testing.T) {
|
||||
assert.Equal(t, UnknownSummaryKey, sut.GetKey(SummaryEditor))
|
||||
assert.Equal(t, UnknownSummaryKey, sut.GetKey(255))
|
||||
}
|
||||
|
||||
func TestHeartbeat_Hashed(t *testing.T) {
|
||||
var sut1, sut2 *Heartbeat
|
||||
|
||||
// same hash if only non-hashed fields are different
|
||||
sut1 = &Heartbeat{Entity: "file1", Editor: "vscode", Time: CustomTime(time.Unix(1673810732, 0))}
|
||||
sut2 = &Heartbeat{Entity: "file1", Editor: "goland", Time: CustomTime(time.Unix(1673810732, 0))}
|
||||
assert.Equal(t, sut1.Hashed().Hash, sut2.Hashed().Hash)
|
||||
|
||||
// different hash if time is different
|
||||
sut1 = &Heartbeat{Entity: "file1", Editor: "vscode", Time: CustomTime(time.Unix(1673810732, 0))}
|
||||
sut2 = &Heartbeat{Entity: "file1", Editor: "goland", Time: CustomTime(time.Unix(1673810733, 0))}
|
||||
assert.NotEqual(t, sut1.Hashed().Hash, sut2.Hashed().Hash)
|
||||
|
||||
// different hash if any other hashed field is different
|
||||
sut1 = &Heartbeat{Entity: "file1", Editor: "vscode", Time: CustomTime(time.Unix(1673810732, 0))}
|
||||
sut2 = &Heartbeat{Entity: "file2", Editor: "goland", Time: CustomTime(time.Unix(1673810732, 0))}
|
||||
assert.NotEqual(t, sut1.Hashed().Hash, sut2.Hashed().Hash)
|
||||
}
|
||||
|
||||
func TestHeartbeat_Hashed_NoCollision(t *testing.T) {
|
||||
hashes := map[string]bool{}
|
||||
|
||||
for i := 0; i < 2500; i++ {
|
||||
sut := &Heartbeat{
|
||||
UserID: "gopher",
|
||||
Entity: "~/dev/wakapi",
|
||||
Type: "file",
|
||||
Category: "coding",
|
||||
Project: "wakapi",
|
||||
Branch: "master",
|
||||
Language: "go",
|
||||
IsWrite: false,
|
||||
Time: CustomTime(time.Unix(1673810732+int64(i), 0)),
|
||||
}
|
||||
assert.NotContains(t, hashes, sut.Hashed().Hash)
|
||||
hashes[sut.Hash] = true
|
||||
}
|
||||
}
|
||||
|
18
models/init_test.go
Normal file
18
models/init_test.go
Normal file
@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// move to project root as working directory (e.g. so data/* can be resolved when loading config)
|
||||
// taken from https://intellij-support.jetbrains.com/hc/en-us/community/posts/360009685279-Go-test-working-directory-keeps-changing-to-dir-of-the-test-file-instead-of-value-in-template
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
dir := path.Join(path.Dir(filename), "..")
|
||||
err := os.Chdir(dir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
@ -14,8 +14,9 @@ var (
|
||||
IntervalPast7DaysYesterday = &IntervalKey{"Last 7 Days from Yesterday"}
|
||||
IntervalPast14Days = &IntervalKey{"Last 14 Days"}
|
||||
IntervalPast30Days = &IntervalKey{"30_days", "last_30_days", "Last 30 Days"}
|
||||
IntervalPast12Months = &IntervalKey{"12_months", "last_12_months"}
|
||||
IntervalAny = &IntervalKey{"any"}
|
||||
IntervalPast6Months = &IntervalKey{"6_months", "last_6_months"}
|
||||
IntervalPast12Months = &IntervalKey{"12_months", "last_12_months", "last_year"}
|
||||
IntervalAny = &IntervalKey{"any", "all_time"}
|
||||
)
|
||||
|
||||
var AllIntervals = []*IntervalKey{
|
||||
@ -30,6 +31,7 @@ var AllIntervals = []*IntervalKey{
|
||||
IntervalPast7DaysYesterday,
|
||||
IntervalPast14Days,
|
||||
IntervalPast30Days,
|
||||
IntervalPast6Months,
|
||||
IntervalPast12Months,
|
||||
IntervalAny,
|
||||
}
|
||||
|
109
models/leaderboard.go
Normal file
109
models/leaderboard.go
Normal file
@ -0,0 +1,109 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/maputil"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LeaderboardItem struct {
|
||||
ID uint `json:"-" gorm:"primary_key; size:32"`
|
||||
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
UserID string `json:"user_id" gorm:"not null; index:idx_leaderboard_user"`
|
||||
Interval string `json:"interval" gorm:"not null; size:32; index:idx_leaderboard_combined"`
|
||||
By *uint8 `json:"aggregated_by" gorm:"index:idx_leaderboard_combined"` // pointer because nullable
|
||||
Total time.Duration `json:"total" gorm:"not null" swaggertype:"primitive,integer"`
|
||||
Key *string `json:"key" gorm:"size:255"` // pointer because nullable
|
||||
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
}
|
||||
|
||||
// https://github.com/go-gorm/gorm/issues/5789
|
||||
// https://github.com/go-gorm/gorm/issues/5284#issuecomment-1107775806
|
||||
type LeaderboardItemRanked struct {
|
||||
LeaderboardItem
|
||||
Rank uint
|
||||
}
|
||||
|
||||
func (l1 *LeaderboardItemRanked) Equals(l2 *LeaderboardItemRanked) bool {
|
||||
return l1.ID == l2.ID
|
||||
}
|
||||
|
||||
type Leaderboard []*LeaderboardItemRanked
|
||||
|
||||
func (l *Leaderboard) Add(item *LeaderboardItemRanked) {
|
||||
if _, found := slice.Find[*LeaderboardItemRanked](*l, func(i int, item2 *LeaderboardItemRanked) bool {
|
||||
return item.Equals(item2)
|
||||
}); !found {
|
||||
*l = append(*l, item)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Leaderboard) AddMany(items []*LeaderboardItemRanked) {
|
||||
for _, item := range items {
|
||||
l.Add(item)
|
||||
}
|
||||
}
|
||||
|
||||
func (l Leaderboard) UserIDs() []string {
|
||||
return slice.Unique[string](slice.Map[*LeaderboardItemRanked, string](l, func(i int, item *LeaderboardItemRanked) string {
|
||||
return item.UserID
|
||||
}))
|
||||
}
|
||||
|
||||
func (l Leaderboard) HasUser(userId string) bool {
|
||||
return slice.Contain(l.UserIDs(), userId)
|
||||
}
|
||||
|
||||
func (l Leaderboard) TopByKey(by uint8, key string) Leaderboard {
|
||||
return slice.Filter[*LeaderboardItemRanked](l, func(i int, item *LeaderboardItemRanked) bool {
|
||||
return item.By != nil && *item.By == by && item.Key != nil && strings.ToLower(*item.Key) == strings.ToLower(key)
|
||||
})
|
||||
}
|
||||
|
||||
func (l Leaderboard) TopKeys(by uint8) []string {
|
||||
type keyTotal struct {
|
||||
Key string
|
||||
Total time.Duration
|
||||
}
|
||||
|
||||
totalsMapped := make(map[string]*keyTotal, len(l))
|
||||
|
||||
for _, item := range l {
|
||||
if item.Key == nil || item.By == nil || *item.By != by {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(*item.Key)
|
||||
if _, ok := totalsMapped[key]; !ok {
|
||||
totalsMapped[key] = &keyTotal{Key: *item.Key, Total: 0}
|
||||
}
|
||||
totalsMapped[key].Total += item.Total
|
||||
}
|
||||
|
||||
totals := slice.Map[*keyTotal, keyTotal](maputil.Values[string, *keyTotal](totalsMapped), func(i int, item *keyTotal) keyTotal {
|
||||
return *item
|
||||
})
|
||||
if err := slice.SortByField(totals, "Total", "desc"); err != nil {
|
||||
return []string{} // TODO
|
||||
}
|
||||
|
||||
return slice.Map[keyTotal, string](totals, func(i int, item keyTotal) string {
|
||||
return item.Key
|
||||
})
|
||||
}
|
||||
|
||||
func (l Leaderboard) TopKeysByUser(by uint8, userId string) []string {
|
||||
return Leaderboard(slice.Filter[*LeaderboardItemRanked](l, func(i int, item *LeaderboardItemRanked) bool {
|
||||
return item.UserID == userId
|
||||
})).TopKeys(by)
|
||||
}
|
||||
|
||||
func (l Leaderboard) LastUpdate() time.Time {
|
||||
lastUpdate := time.Time{}
|
||||
for _, item := range l {
|
||||
if item.CreatedAt.T().After(lastUpdate) {
|
||||
lastUpdate = item.CreatedAt.T()
|
||||
}
|
||||
}
|
||||
return lastUpdate
|
||||
}
|
@ -4,7 +4,7 @@ import "fmt"
|
||||
|
||||
type CounterMetric struct {
|
||||
Name string
|
||||
Value int
|
||||
Value int64
|
||||
Desc string
|
||||
Labels Labels
|
||||
}
|
||||
|
22
models/metrics/gauge_metric.go
Normal file
22
models/metrics/gauge_metric.go
Normal file
@ -0,0 +1,22 @@
|
||||
package metrics
|
||||
|
||||
import "fmt"
|
||||
|
||||
type GaugeMetric struct {
|
||||
Name string
|
||||
Value int64
|
||||
Desc string
|
||||
Labels Labels
|
||||
}
|
||||
|
||||
func (c GaugeMetric) Key() string {
|
||||
return c.Name
|
||||
}
|
||||
|
||||
func (c GaugeMetric) Print() string {
|
||||
return fmt.Sprintf("%s%s %d", c.Name, c.Labels.Print(), c.Value)
|
||||
}
|
||||
|
||||
func (c GaugeMetric) Header() string {
|
||||
return fmt.Sprintf("# HELP %s %s\n# TYPE %s gauge", c.Name, c.Desc, c.Name)
|
||||
}
|
@ -5,20 +5,18 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
UserKey = "user"
|
||||
ImprintKey = "imprint"
|
||||
AuthCookieKey = "wakapi_auth"
|
||||
UserKey = "user"
|
||||
ImprintKey = "imprint"
|
||||
AuthCookieKey = "wakapi_auth"
|
||||
PersistentIntervalKey = "wakapi_summary_interval"
|
||||
)
|
||||
|
||||
type MigrationFunc func(db *gorm.DB) error
|
||||
|
||||
type KeyStringValue struct {
|
||||
Key string `gorm:"primary_key"`
|
||||
Value string `gorm:"type:text"`
|
||||
@ -29,6 +27,11 @@ type Interval struct {
|
||||
End time.Time
|
||||
}
|
||||
|
||||
type KeyedInterval struct {
|
||||
Interval
|
||||
Key *IntervalKey
|
||||
}
|
||||
|
||||
// CustomTime is a wrapper type around time.Time, mainly used for the purpose of transparently unmarshalling Python timestamps in the format <sec>.<nsec> (e.g. 1619335137.3324468)
|
||||
type CustomTime time.Time
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
const (
|
||||
NSummaryTypes uint8 = 99
|
||||
SummaryUnknown uint8 = 98
|
||||
SummaryProject uint8 = 0
|
||||
SummaryLanguage uint8 = 1
|
||||
SummaryEditor uint8 = 2
|
||||
@ -21,7 +22,7 @@ const UnknownSummaryKey = "unknown"
|
||||
const DefaultProjectLabel = "default"
|
||||
|
||||
type Summary struct {
|
||||
ID uint `json:"-" gorm:"primary_key"`
|
||||
ID uint `json:"-" gorm:"primary_key; size:32"`
|
||||
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
UserID string `json:"user_id" gorm:"not null; index:idx_time_summary_user"`
|
||||
FromTime CustomTime `json:"from" gorm:"not null; type:timestamp; default:CURRENT_TIMESTAMP; index:idx_time_summary_user" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
@ -33,7 +34,7 @@ type Summary struct {
|
||||
Machines SummaryItems `json:"machines" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
Labels SummaryItems `json:"labels" gorm:"-"` // labels are not persisted, but calculated at runtime, i.e. when summary is retrieved
|
||||
Branches SummaryItems `json:"branches" gorm:"-"` // branches are not persisted, but calculated at runtime in case a project filter is applied
|
||||
NumHeartbeats int `json:"-" gorm:"default:0"`
|
||||
NumHeartbeats int `json:"-"`
|
||||
}
|
||||
|
||||
type SummaryItems []*SummaryItem
|
||||
@ -41,9 +42,9 @@ type SummaryItems []*SummaryItem
|
||||
type SummaryItem struct {
|
||||
ID uint64 `json:"-" gorm:"primary_key"`
|
||||
Summary *Summary `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
SummaryID uint `json:"-"`
|
||||
SummaryID uint `json:"-" gorm:"size:32"`
|
||||
Type uint8 `json:"-" gorm:"index:idx_type"`
|
||||
Key string `json:"key"`
|
||||
Key string `json:"key" gorm:"size:255"`
|
||||
Total time.Duration `json:"total" swaggertype:"primitive,integer"`
|
||||
}
|
||||
|
||||
@ -100,10 +101,42 @@ func (s *Summary) MappedItems() map[uint8]*SummaryItems {
|
||||
}
|
||||
|
||||
func (s *Summary) ItemsByType(summaryType uint8) *SummaryItems {
|
||||
return s.MappedItems()[summaryType]
|
||||
switch summaryType {
|
||||
case SummaryProject:
|
||||
return &s.Projects
|
||||
case SummaryLanguage:
|
||||
return &s.Languages
|
||||
case SummaryEditor:
|
||||
return &s.Editors
|
||||
case SummaryOS:
|
||||
return &s.OperatingSystems
|
||||
case SummaryMachine:
|
||||
return &s.Machines
|
||||
case SummaryLabel:
|
||||
return &s.Labels
|
||||
case SummaryBranch:
|
||||
return &s.Branches
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/* Augments the summary in a way that at least one item is present for every type.
|
||||
func (s *Summary) KeepOnly(types map[uint8]bool) *Summary {
|
||||
if len(types) == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
for _, t := range SummaryTypes() {
|
||||
if keep, ok := types[t]; !keep || !ok {
|
||||
*s.ItemsByType(t) = []*SummaryItem{}
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
/*
|
||||
Augments the summary in a way that at least one item is present for every type.
|
||||
|
||||
If a summary has zero items for a given type, but one or more for any of the other types,
|
||||
the total summary duration can be derived from those and inserted as a dummy-item with key "unknown"
|
||||
for the missing type.
|
||||
|
@ -168,6 +168,66 @@ func TestSummary_WithResolvedAliases(t *testing.T) {
|
||||
assert.Empty(t, sut.Machines)
|
||||
}
|
||||
|
||||
func TestSummary_KeepOnly(t *testing.T) {
|
||||
newSummary := func() *Summary {
|
||||
return &Summary{
|
||||
Projects: []*SummaryItem{
|
||||
{
|
||||
Type: SummaryProject,
|
||||
Key: "wakapi",
|
||||
// hack to work around the issue that the total time of a summary item is mistakenly represented in seconds
|
||||
Total: 10 * time.Minute / time.Second,
|
||||
},
|
||||
{
|
||||
Type: SummaryProject,
|
||||
Key: "anchr",
|
||||
Total: 10 * time.Minute / time.Second,
|
||||
},
|
||||
},
|
||||
Languages: []*SummaryItem{
|
||||
{
|
||||
Type: SummaryLanguage,
|
||||
Key: "Go",
|
||||
Total: 10 * time.Minute / time.Second,
|
||||
},
|
||||
},
|
||||
Editors: []*SummaryItem{
|
||||
{
|
||||
Type: SummaryEditor,
|
||||
Key: "VSCode",
|
||||
Total: 10 * time.Minute / time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var sut *Summary
|
||||
|
||||
sut = newSummary().KeepOnly(map[uint8]bool{}) // keep all
|
||||
assert.NotZero(t, sut.TotalTimeBy(SummaryProject))
|
||||
assert.NotZero(t, sut.TotalTimeBy(SummaryLanguage))
|
||||
assert.NotZero(t, sut.TotalTimeBy(SummaryEditor))
|
||||
assert.Equal(t, 20*time.Minute, sut.TotalTime())
|
||||
|
||||
sut = newSummary().KeepOnly(map[uint8]bool{SummaryProject: true})
|
||||
assert.NotZero(t, sut.TotalTimeBy(SummaryProject))
|
||||
assert.Zero(t, sut.TotalTimeBy(SummaryLanguage))
|
||||
assert.Zero(t, sut.TotalTimeBy(SummaryEditor))
|
||||
assert.Equal(t, 20*time.Minute, sut.TotalTime())
|
||||
|
||||
sut = newSummary().KeepOnly(map[uint8]bool{SummaryEditor: true, SummaryLanguage: true})
|
||||
assert.Zero(t, sut.TotalTimeBy(SummaryProject))
|
||||
assert.NotZero(t, sut.TotalTimeBy(SummaryLanguage))
|
||||
assert.NotZero(t, sut.TotalTimeBy(SummaryEditor))
|
||||
assert.Equal(t, 10*time.Minute, sut.TotalTime())
|
||||
|
||||
sut = newSummary().KeepOnly(map[uint8]bool{SummaryProject: true})
|
||||
sut.FillMissing()
|
||||
assert.NotZero(t, sut.TotalTimeBy(SummaryProject))
|
||||
assert.NotZero(t, sut.TotalTimeBy(SummaryLanguage))
|
||||
assert.NotZero(t, sut.TotalTimeBy(SummaryEditor))
|
||||
}
|
||||
|
||||
func TestSummaryItems_Sorted(t *testing.T) {
|
||||
testDuration1, testDuration2, testDuration3 := 10*time.Minute, 5*time.Minute, 20*time.Minute
|
||||
|
||||
|
@ -3,6 +3,8 @@ package models
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@ -13,26 +15,29 @@ func init() {
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id" gorm:"primary_key"`
|
||||
ApiKey string `json:"api_key" gorm:"unique"`
|
||||
Email string `json:"email" gorm:"index:idx_user_email; size:255"`
|
||||
Location string `json:"location"`
|
||||
Password string `json:"-"`
|
||||
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
ShareDataMaxDays int `json:"-" gorm:"default:0"`
|
||||
ShareEditors bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ShareLanguages bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ShareProjects bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ShareOSs bool `json:"-" gorm:"default:false; type:bool; column:share_oss"`
|
||||
ShareMachines bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ShareLabels bool `json:"-" gorm:"default:false; type:bool"`
|
||||
IsAdmin bool `json:"-" gorm:"default:false; type:bool"`
|
||||
HasData bool `json:"-" gorm:"default:false; type:bool"`
|
||||
WakatimeApiKey string `json:"-"` // for relay middleware and imports
|
||||
WakatimeApiUrl string `json:"-"` // for relay middleware and imports
|
||||
ResetToken string `json:"-"`
|
||||
ReportsWeekly bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ID string `json:"id" gorm:"primary_key"`
|
||||
ApiKey string `json:"api_key" gorm:"unique; default:NULL"`
|
||||
Email string `json:"email" gorm:"index:idx_user_email; size:255"`
|
||||
Location string `json:"location"`
|
||||
Password string `json:"-"`
|
||||
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
ShareDataMaxDays int `json:"-"`
|
||||
ShareEditors bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ShareLanguages bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ShareProjects bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ShareOSs bool `json:"-" gorm:"default:false; type:bool; column:share_oss"`
|
||||
ShareMachines bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ShareLabels bool `json:"-" gorm:"default:false; type:bool"`
|
||||
IsAdmin bool `json:"-" gorm:"default:false; type:bool"`
|
||||
HasData bool `json:"-" gorm:"default:false; type:bool"`
|
||||
WakatimeApiKey string `json:"-"` // for relay middleware and imports
|
||||
WakatimeApiUrl string `json:"-"` // for relay middleware and imports
|
||||
ResetToken string `json:"-"`
|
||||
ReportsWeekly bool `json:"-" gorm:"default:false; type:bool"`
|
||||
PublicLeaderboard bool `json:"-" gorm:"default:false; type:bool"`
|
||||
SubscribedUntil *CustomTime `json:"-" gorm:"type:timestamp" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
StripeCustomerId string `json:"-"`
|
||||
}
|
||||
|
||||
type Login struct {
|
||||
@ -65,9 +70,10 @@ type CredentialsReset struct {
|
||||
}
|
||||
|
||||
type UserDataUpdate struct {
|
||||
Email string `schema:"email"`
|
||||
Location string `schema:"location"`
|
||||
ReportsWeekly bool `schema:"reports_weekly"`
|
||||
Email string `schema:"email"`
|
||||
Location string `schema:"location"`
|
||||
ReportsWeekly bool `schema:"reports_weekly"`
|
||||
PublicLeaderboard bool `schema:"public_leaderboard"`
|
||||
}
|
||||
|
||||
type TimeByUser struct {
|
||||
@ -80,6 +86,10 @@ type CountByUser struct {
|
||||
Count int64
|
||||
}
|
||||
|
||||
func (u *User) Identity() string {
|
||||
return u.ID
|
||||
}
|
||||
|
||||
func (u *User) TZ() *time.Location {
|
||||
if u.Location == "" {
|
||||
u.Location = "Local"
|
||||
@ -118,6 +128,40 @@ func (u *User) WakaTimeURL(fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// HasActiveSubscription returns true if subscriptions are enabled on the server and the user has got one
|
||||
func (u *User) HasActiveSubscription() bool {
|
||||
return conf.Get().Subscriptions.Enabled && u.HasActiveSubscriptionStrict()
|
||||
}
|
||||
|
||||
func (u *User) HasActiveSubscriptionStrict() bool {
|
||||
return u.SubscribedUntil != nil && u.SubscribedUntil.T().After(time.Now())
|
||||
}
|
||||
|
||||
// SubscriptionExpiredSince returns if a user's subscription has expiration and the duration since when that happened.
|
||||
// Returns (false, <negative duration>), if subscription hasn't expired, yet.
|
||||
// Returns (false, 0), if subscriptions are not enabled in the first place.
|
||||
// Returns (true, <very long duration>), if the user has never had a subscription.
|
||||
func (u *User) SubscriptionExpiredSince() (bool, time.Duration) {
|
||||
cfg := conf.Get()
|
||||
if !cfg.Subscriptions.Enabled {
|
||||
return false, 0
|
||||
}
|
||||
if u.SubscribedUntil == nil {
|
||||
return true, 99 * 365 * 24 * time.Hour
|
||||
}
|
||||
diff := time.Now().Sub(u.SubscribedUntil.T())
|
||||
return diff >= 0, diff
|
||||
}
|
||||
|
||||
func (u *User) MinDataAge() time.Time {
|
||||
retentionMonths := conf.Get().App.DataRetentionMonths
|
||||
if retentionMonths <= 0 || u.HasActiveSubscription() {
|
||||
return time.Time{}
|
||||
}
|
||||
// this is not exactly precise, because of summer / winter time, etc.
|
||||
return time.Now().AddDate(0, -retentionMonths, 0)
|
||||
}
|
||||
|
||||
func (c *CredentialsReset) IsValid() bool {
|
||||
return ValidatePassword(c.PasswordNew) &&
|
||||
c.PasswordNew == c.PasswordRepeat
|
||||
@ -147,8 +191,9 @@ func ValidatePassword(password string) bool {
|
||||
return len(password) >= 6
|
||||
}
|
||||
|
||||
// ValidateEmail checks that, if an email address is given, it has proper syntax and (if not in dev mode) an MX record exists for the domain
|
||||
func ValidateEmail(email string) bool {
|
||||
return email == "" || mailRegex.Match([]byte(email))
|
||||
return email == "" || (mailRegex.Match([]byte(email)) && (conf.Get().IsDev() || utils.CheckEmailMX(email)))
|
||||
}
|
||||
|
||||
func ValidateTimezone(tz string) bool {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
@ -18,3 +19,41 @@ func TestUser_TZ(t *testing.T) {
|
||||
assert.InDelta(t, time.Duration(offset1*int(time.Second)), sut1.TZOffset(), float64(1*time.Second))
|
||||
assert.InDelta(t, time.Duration(offset2*int(time.Second)), sut2.TZOffset(), float64(1*time.Second))
|
||||
}
|
||||
|
||||
func TestUser_MinDataAge(t *testing.T) {
|
||||
c := conf.Load("")
|
||||
|
||||
var sut *User
|
||||
|
||||
// test with unlimited retention time / clean-up disabled
|
||||
c.App.DataRetentionMonths = -1
|
||||
c.Subscriptions.Enabled = false
|
||||
sut = &User{}
|
||||
assert.Zero(t, sut.MinDataAge())
|
||||
|
||||
// test with limited retention time / clean-up enabled, and subscriptions disabled
|
||||
c.App.DataRetentionMonths = 1
|
||||
c.Subscriptions.Enabled = false
|
||||
sut = &User{}
|
||||
assert.WithinRange(t, sut.MinDataAge(), time.Now().AddDate(0, -1, -1), time.Now().AddDate(0, -1, 1))
|
||||
|
||||
// test with limited retention time, subscriptions enabled, and user hasn't got one
|
||||
c.App.DataRetentionMonths = 1
|
||||
c.Subscriptions.Enabled = true
|
||||
sut = &User{}
|
||||
assert.WithinRange(t, sut.MinDataAge(), time.Now().AddDate(0, -1, -1), time.Now().AddDate(0, -1, 1))
|
||||
|
||||
// test with limited retention time, subscriptions disabled, but user still got (an expired) one
|
||||
c.App.DataRetentionMonths = 1
|
||||
c.Subscriptions.Enabled = false
|
||||
until2 := CustomTime(time.Now().AddDate(0, 0, -1))
|
||||
sut = &User{SubscribedUntil: &until2}
|
||||
assert.WithinRange(t, sut.MinDataAge(), time.Now().AddDate(0, -1, -1), time.Now().AddDate(0, -1, 1))
|
||||
|
||||
// test with limited retention time, subscriptions enabled, and user has got one
|
||||
c.App.DataRetentionMonths = 1
|
||||
c.Subscriptions.Enabled = true
|
||||
until1 := CustomTime(time.Now().AddDate(0, 1, 0))
|
||||
sut = &User{SubscribedUntil: &until1}
|
||||
assert.Zero(t, sut.MinDataAge())
|
||||
}
|
||||
|
19
models/view/common.go
Normal file
19
models/view/common.go
Normal file
@ -0,0 +1,19 @@
|
||||
package view
|
||||
|
||||
type BasicViewModel interface {
|
||||
SetError(string)
|
||||
SetSuccess(string)
|
||||
}
|
||||
|
||||
type Messages struct {
|
||||
Success string
|
||||
Error string
|
||||
}
|
||||
|
||||
func (m *Messages) SetError(message string) {
|
||||
m.Error = message
|
||||
}
|
||||
|
||||
func (m *Messages) SetSuccess(message string) {
|
||||
m.Success = message
|
||||
}
|
@ -1,18 +1,23 @@
|
||||
package view
|
||||
|
||||
type Newsbox struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type HomeViewModel struct {
|
||||
Success string
|
||||
Error string
|
||||
Messages
|
||||
TotalHours int
|
||||
TotalUsers int
|
||||
Newsbox *Newsbox
|
||||
}
|
||||
|
||||
func (s *HomeViewModel) WithSuccess(m string) *HomeViewModel {
|
||||
s.Success = m
|
||||
s.SetSuccess(m)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *HomeViewModel) WithError(m string) *HomeViewModel {
|
||||
s.Error = m
|
||||
s.SetError(m)
|
||||
return s
|
||||
}
|
||||
|
@ -1,18 +1,17 @@
|
||||
package view
|
||||
|
||||
type ImprintViewModel struct {
|
||||
Messages
|
||||
HtmlText string
|
||||
Success string
|
||||
Error string
|
||||
}
|
||||
|
||||
func (s *ImprintViewModel) WithSuccess(m string) *ImprintViewModel {
|
||||
s.Success = m
|
||||
s.SetSuccess(m)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *ImprintViewModel) WithError(m string) *ImprintViewModel {
|
||||
s.Error = m
|
||||
s.SetError(m)
|
||||
return s
|
||||
}
|
||||
|
||||
|
83
models/view/leaderboard.go
Normal file
83
models/view/leaderboard.go
Normal file
@ -0,0 +1,83 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LeaderboardViewModel struct {
|
||||
Messages
|
||||
User *models.User
|
||||
By string
|
||||
Key string
|
||||
Items []*models.LeaderboardItemRanked
|
||||
TopKeys []string
|
||||
UserLanguages map[string][]string
|
||||
ApiKey string
|
||||
PageParams *utils.PageParams
|
||||
}
|
||||
|
||||
func (s *LeaderboardViewModel) WithSuccess(m string) *LeaderboardViewModel {
|
||||
s.SetSuccess(m)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *LeaderboardViewModel) WithError(m string) *LeaderboardViewModel {
|
||||
s.SetError(m)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *LeaderboardViewModel) ColorModifier(item *models.LeaderboardItemRanked, principal *models.User) string {
|
||||
if principal != nil && item.UserID == principal.ID {
|
||||
return "self"
|
||||
}
|
||||
if item.Rank == 1 {
|
||||
return "gold"
|
||||
}
|
||||
if item.Rank == 2 {
|
||||
return "silver"
|
||||
}
|
||||
if item.Rank == 3 {
|
||||
return "bronze"
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
|
||||
func (s *LeaderboardViewModel) LangIcon(lang string) string {
|
||||
// https://icon-sets.iconify.design/mdi/
|
||||
langs := map[string]string{
|
||||
"c++": "language-cpp",
|
||||
"cpp": "language-cpp",
|
||||
"go": "language-go",
|
||||
"haskell": "language-haskell",
|
||||
"html": "language-html5",
|
||||
"java": "language-java",
|
||||
"javascript": "language-javascript",
|
||||
"jsx": "language-javascript",
|
||||
"kotlin": "language-kotlin",
|
||||
"lua": "language-lua",
|
||||
"php": "language-php",
|
||||
"python": "language-python",
|
||||
"r": "language-r",
|
||||
"ruby": "language-ruby",
|
||||
"rust": "language-rust",
|
||||
"swift": "language-swift",
|
||||
"typescript": "language-typescript",
|
||||
"tsx": "language-typescript",
|
||||
"markdown": "language-markdown",
|
||||
"vue": "vuejs",
|
||||
"react": "react",
|
||||
"bash": "bash",
|
||||
"json": "code-json",
|
||||
}
|
||||
if match, ok := langs[strings.ToLower(lang)]; ok {
|
||||
return "mdi:" + match
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *LeaderboardViewModel) LastUpdate() time.Time {
|
||||
return models.Leaderboard(s.Items).LastUpdate()
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
package view
|
||||
|
||||
type LoginViewModel struct {
|
||||
Success string
|
||||
Error string
|
||||
Messages
|
||||
TotalUsers int
|
||||
AllowSignup bool
|
||||
}
|
||||
@ -13,11 +12,11 @@ type SetPasswordViewModel struct {
|
||||
}
|
||||
|
||||
func (s *LoginViewModel) WithSuccess(m string) *LoginViewModel {
|
||||
s.Success = m
|
||||
s.SetSuccess(m)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *LoginViewModel) WithError(m string) *LoginViewModel {
|
||||
s.Error = m
|
||||
s.SetError(m)
|
||||
return s
|
||||
}
|
||||
|
@ -1,16 +1,22 @@
|
||||
package view
|
||||
|
||||
import "github.com/muety/wakapi/models"
|
||||
import (
|
||||
"github.com/muety/wakapi/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SettingsViewModel struct {
|
||||
User *models.User
|
||||
LanguageMappings []*models.LanguageMapping
|
||||
Aliases []*SettingsVMCombinedAlias
|
||||
Labels []*SettingsVMCombinedLabel
|
||||
Projects []string
|
||||
ApiKey string
|
||||
Success string
|
||||
Error string
|
||||
Messages
|
||||
User *models.User
|
||||
LanguageMappings []*models.LanguageMapping
|
||||
Aliases []*SettingsVMCombinedAlias
|
||||
Labels []*SettingsVMCombinedLabel
|
||||
Projects []string
|
||||
SubscriptionPrice string
|
||||
DataRetentionMonths int
|
||||
UserFirstData time.Time
|
||||
SupportContact string
|
||||
ApiKey string
|
||||
}
|
||||
|
||||
type SettingsVMCombinedAlias struct {
|
||||
@ -24,12 +30,16 @@ type SettingsVMCombinedLabel struct {
|
||||
Values []string
|
||||
}
|
||||
|
||||
func (s *SettingsViewModel) SubscriptionsEnabled() bool {
|
||||
return s.SubscriptionPrice != ""
|
||||
}
|
||||
|
||||
func (s *SettingsViewModel) WithSuccess(m string) *SettingsViewModel {
|
||||
s.Success = m
|
||||
s.SetSuccess(m)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *SettingsViewModel) WithError(m string) *SettingsViewModel {
|
||||
s.Error = m
|
||||
s.SetError(m)
|
||||
return s
|
||||
}
|
||||
|
@ -1,25 +1,40 @@
|
||||
package view
|
||||
|
||||
import "github.com/muety/wakapi/models"
|
||||
import (
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SummaryViewModel struct {
|
||||
Messages
|
||||
*models.Summary
|
||||
*models.SummaryParams
|
||||
User *models.User
|
||||
AvatarURL string
|
||||
EditorColors map[string]string
|
||||
LanguageColors map[string]string
|
||||
Error string
|
||||
Success string
|
||||
OSColors map[string]string
|
||||
ApiKey string
|
||||
RawQuery string
|
||||
UserFirstData time.Time
|
||||
}
|
||||
|
||||
func (s SummaryViewModel) UserDataExpiring() bool {
|
||||
cfg := conf.Get()
|
||||
return cfg.Subscriptions.Enabled &&
|
||||
cfg.App.DataRetentionMonths > 0 &&
|
||||
!s.UserFirstData.IsZero() &&
|
||||
!s.User.HasActiveSubscription() &&
|
||||
time.Now().AddDate(0, -cfg.App.DataRetentionMonths, 0).After(s.UserFirstData)
|
||||
}
|
||||
|
||||
func (s *SummaryViewModel) WithSuccess(m string) *SummaryViewModel {
|
||||
s.Success = m
|
||||
s.SetSuccess(m)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *SummaryViewModel) WithError(m string) *SummaryViewModel {
|
||||
s.Error = m
|
||||
s.SetError(m)
|
||||
return s
|
||||
}
|
||||
|
@ -9,10 +9,10 @@
|
||||
"compress": "brotli -f static/assets/css/*.dist.css && brotli -f static/assets/js/*.dist.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^1.1.444",
|
||||
"@iconify/json": "^2.1.136",
|
||||
"@iconify/json-tools": "^1.0.10",
|
||||
"chokidar-cli": "^3.0.0",
|
||||
"tailwindcss": "2.2.19"
|
||||
"tailwindcss": "^3.1.8"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"errors"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
@ -9,11 +9,12 @@ import (
|
||||
)
|
||||
|
||||
type HeartbeatRepository struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
config *conf.Config
|
||||
}
|
||||
|
||||
func NewHeartbeatRepository(db *gorm.DB) *HeartbeatRepository {
|
||||
return &HeartbeatRepository{db: db}
|
||||
return &HeartbeatRepository{config: conf.Get(), db: db}
|
||||
}
|
||||
|
||||
// Use with caution!!
|
||||
@ -77,6 +78,26 @@ func (r *HeartbeatRepository) GetAllWithin(from, to time.Time, user *models.User
|
||||
return heartbeats, nil
|
||||
}
|
||||
|
||||
func (r *HeartbeatRepository) GetAllWithinByFilters(from, to time.Time, user *models.User, filterMap map[string][]string) ([]*models.Heartbeat, error) {
|
||||
// https://stackoverflow.com/a/20765152/3112139
|
||||
var heartbeats []*models.Heartbeat
|
||||
|
||||
q := r.db.
|
||||
Where(&models.Heartbeat{UserID: user.ID}).
|
||||
Where("time >= ?", from.Local()).
|
||||
Where("time < ?", to.Local()).
|
||||
Order("time asc")
|
||||
|
||||
for col, vals := range filterMap {
|
||||
q = q.Where(col+" in ?", vals)
|
||||
}
|
||||
|
||||
if err := q.Find(&heartbeats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return heartbeats, nil
|
||||
}
|
||||
|
||||
func (r *HeartbeatRepository) GetFirstByUsers() ([]*models.TimeByUser, error) {
|
||||
var result []*models.TimeByUser
|
||||
r.db.Model(&models.User{}).
|
||||
@ -97,12 +118,19 @@ func (r *HeartbeatRepository) GetLastByUsers() ([]*models.TimeByUser, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *HeartbeatRepository) Count() (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.
|
||||
Model(&models.Heartbeat{}).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
func (r *HeartbeatRepository) Count(approximate bool) (count int64, err error) {
|
||||
if r.config.Db.IsMySQL() && approximate {
|
||||
err = r.db.Table("information_schema.tables").
|
||||
Select("table_rows").
|
||||
Where("table_schema = ?", r.config.Db.Name).
|
||||
Where("table_name = 'heartbeats'").
|
||||
Scan(&count).Error
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
err = r.db.
|
||||
Model(&models.Heartbeat{}).
|
||||
Count(&count).Error
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
@ -126,6 +154,10 @@ func (r *HeartbeatRepository) CountByUsers(users []*models.User) ([]*models.Coun
|
||||
userIds[i] = u.ID
|
||||
}
|
||||
|
||||
if len(userIds) == 0 {
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
if err := r.db.
|
||||
Model(&models.Heartbeat{}).
|
||||
Select("user_id as user, count(id) as count").
|
||||
@ -134,20 +166,15 @@ func (r *HeartbeatRepository) CountByUsers(users []*models.User) ([]*models.Coun
|
||||
Find(&counts).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (r HeartbeatRepository) GetEntitySetByUser(entityType uint8, user *models.User) ([]string, error) {
|
||||
columns := []string{"project", "language", "editor", "operating_system", "machine"}
|
||||
if int(entityType) >= len(columns) {
|
||||
// invalid entity type
|
||||
return nil, errors.New("invalid entity type")
|
||||
}
|
||||
|
||||
func (r *HeartbeatRepository) GetEntitySetByUser(entityType uint8, user *models.User) ([]string, error) {
|
||||
var results []string
|
||||
if err := r.db.
|
||||
Model(&models.Heartbeat{}).
|
||||
Distinct(columns[entityType]).
|
||||
Distinct(models.GetEntityColumn(entityType)).
|
||||
Where(&models.Heartbeat{UserID: user.ID}).
|
||||
Find(&results).Error; err != nil {
|
||||
return nil, err
|
||||
@ -163,3 +190,22 @@ func (r *HeartbeatRepository) DeleteBefore(t time.Time) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *HeartbeatRepository) DeleteByUser(user *models.User) error {
|
||||
if err := r.db.
|
||||
Where("user_id = ?", user.ID).
|
||||
Delete(models.Heartbeat{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *HeartbeatRepository) DeleteByUserBefore(user *models.User, t time.Time) error {
|
||||
if err := r.db.
|
||||
Where("user_id = ?", user.ID).
|
||||
Where("time <= ?", t.Local()).
|
||||
Delete(models.Heartbeat{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
@ -34,6 +34,17 @@ func (r *KeyValueRepository) GetString(key string) (*models.KeyStringValue, erro
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
func (r *KeyValueRepository) Search(like string) ([]*models.KeyStringValue, error) {
|
||||
var keyValues []*models.KeyStringValue
|
||||
if err := r.db.Table("key_string_values").
|
||||
Where("`key` like ?", like).
|
||||
Find(&keyValues).
|
||||
Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return keyValues, nil
|
||||
}
|
||||
|
||||
func (r *KeyValueRepository) PutString(kv *models.KeyStringValue) error {
|
||||
result := r.db.
|
||||
Clauses(clause.OnConflict{
|
||||
|
108
repositories/leaderboard.go
Normal file
108
repositories/leaderboard.go
Normal file
@ -0,0 +1,108 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type LeaderboardRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewLeaderboardRepository(db *gorm.DB) *LeaderboardRepository {
|
||||
return &LeaderboardRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) InsertBatch(items []*models.LeaderboardItem) error {
|
||||
if err := r.db.
|
||||
Clauses(clause.OnConflict{DoNothing: true}).
|
||||
Create(&items).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) CountAllByUser(userId string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.
|
||||
Table("leaderboard_items").
|
||||
Where("user_id = ?", userId).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) CountUsers() (int64, error) {
|
||||
var count int64
|
||||
err := r.db.
|
||||
Table("leaderboard_items").
|
||||
Distinct("user_id").
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) GetAllAggregatedByInterval(key *models.IntervalKey, by *uint8, limit, skip int) ([]*models.LeaderboardItemRanked, error) {
|
||||
// TODO: distinct by (user, key) to filter out potential duplicates ?
|
||||
|
||||
var items []*models.LeaderboardItemRanked
|
||||
subq := r.db.
|
||||
Table("leaderboard_items").
|
||||
Select("*, rank() over (partition by \"key\" order by total desc) as \"rank\"").
|
||||
Where("\"interval\" in ?", *key)
|
||||
subq = utils.WhereNullable(subq, "\"by\"", by)
|
||||
|
||||
q := r.db.Table("(?) as ranked", subq)
|
||||
q = r.withPaging(q, limit, skip)
|
||||
|
||||
if err := q.Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) GetAggregatedByUserAndInterval(userId string, key *models.IntervalKey, by *uint8, limit, skip int) ([]*models.LeaderboardItemRanked, error) {
|
||||
var items []*models.LeaderboardItemRanked
|
||||
subq := r.db.
|
||||
Table("leaderboard_items").
|
||||
Select("*, rank() over (partition by \"key\" order by total desc) as \"rank\"").
|
||||
Where("\"interval\" in ?", *key)
|
||||
subq = utils.WhereNullable(subq, "\"by\"", by)
|
||||
|
||||
q := r.db.Table("(?) as ranked", subq).Where("user_id = ?", userId)
|
||||
q = r.withPaging(q, limit, skip)
|
||||
|
||||
if err := q.Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) DeleteByUser(userId string) error {
|
||||
if err := r.db.
|
||||
Where("user_id = ?", userId).
|
||||
Delete(models.LeaderboardItem{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) DeleteByUserAndInterval(userId string, key *models.IntervalKey) error {
|
||||
if err := r.db.
|
||||
Where("user_id = ?", userId).
|
||||
Where("\"interval\" in ?", *key).
|
||||
Delete(models.LeaderboardItem{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) withPaging(q *gorm.DB, limit, skip int) *gorm.DB {
|
||||
if limit > 0 {
|
||||
q = q.Where("\"rank\" <= ?", skip+limit)
|
||||
}
|
||||
if skip > 0 {
|
||||
q = q.Where("\"rank\" > ?", skip)
|
||||
}
|
||||
return q
|
||||
}
|
43
repositories/metrics.go
Normal file
43
repositories/metrics.go
Normal file
@ -0,0 +1,43 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/config"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MetricsRepository struct {
|
||||
config *config.Config
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
const sizeTplMysql = `
|
||||
SELECT SUM(data_length + index_length)
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = ?
|
||||
GROUP BY table_schema`
|
||||
|
||||
const sizeTplPostgres = `SELECT pg_database_size(?);`
|
||||
|
||||
const sizeTplSqlite = `
|
||||
SELECT page_count * page_size as size
|
||||
FROM pragma_page_count(), pragma_page_size();`
|
||||
|
||||
func NewMetricsRepository(db *gorm.DB) *MetricsRepository {
|
||||
return &MetricsRepository{config: config.Get(), db: db}
|
||||
}
|
||||
|
||||
func (srv *MetricsRepository) GetDatabaseSize() (size int64, err error) {
|
||||
cfg := srv.config.Db
|
||||
|
||||
query := srv.db.Raw("SELECT 0")
|
||||
if cfg.IsMySQL() {
|
||||
query = srv.db.Raw(sizeTplMysql, cfg.Name)
|
||||
} else if cfg.IsPostgres() {
|
||||
query = srv.db.Raw(sizeTplPostgres, cfg.Name)
|
||||
} else if cfg.IsSQLite() {
|
||||
query = srv.db.Raw(sizeTplSqlite)
|
||||
}
|
||||
|
||||
err = query.Scan(&size).Error
|
||||
return size, err
|
||||
}
|
@ -20,15 +20,18 @@ type IHeartbeatRepository interface {
|
||||
InsertBatch([]*models.Heartbeat) error
|
||||
GetAll() ([]*models.Heartbeat, error)
|
||||
GetAllWithin(time.Time, time.Time, *models.User) ([]*models.Heartbeat, error)
|
||||
GetAllWithinByFilters(time.Time, time.Time, *models.User, map[string][]string) ([]*models.Heartbeat, error)
|
||||
GetFirstByUsers() ([]*models.TimeByUser, error)
|
||||
GetLastByUsers() ([]*models.TimeByUser, error)
|
||||
GetLatestByUser(*models.User) (*models.Heartbeat, error)
|
||||
GetLatestByOriginAndUser(string, *models.User) (*models.Heartbeat, error)
|
||||
Count() (int64, error)
|
||||
Count(bool) (int64, error)
|
||||
CountByUser(*models.User) (int64, error)
|
||||
CountByUsers([]*models.User) ([]*models.CountByUser, error)
|
||||
GetEntitySetByUser(uint8, *models.User) ([]string, error)
|
||||
DeleteBefore(time.Time) error
|
||||
DeleteByUser(*models.User) error
|
||||
DeleteByUserBefore(*models.User, time.Time) error
|
||||
}
|
||||
|
||||
type IDiagnosticsRepository interface {
|
||||
@ -40,6 +43,7 @@ type IKeyValueRepository interface {
|
||||
GetString(string) (*models.KeyStringValue, error)
|
||||
PutString(*models.KeyStringValue) error
|
||||
DeleteString(string) error
|
||||
Search(string) ([]*models.KeyStringValue, error)
|
||||
}
|
||||
|
||||
type ILanguageMappingRepository interface {
|
||||
@ -64,16 +68,16 @@ type ISummaryRepository interface {
|
||||
GetByUserWithin(*models.User, time.Time, time.Time) ([]*models.Summary, error)
|
||||
GetLastByUser() ([]*models.TimeByUser, error)
|
||||
DeleteByUser(string) error
|
||||
DeleteByUserBefore(string, time.Time) error
|
||||
}
|
||||
|
||||
type IUserRepository interface {
|
||||
GetById(string) (*models.User, error)
|
||||
FindOne(user models.User) (*models.User, error)
|
||||
GetByIds([]string) ([]*models.User, error)
|
||||
GetByApiKey(string) (*models.User, error)
|
||||
GetByEmail(string) (*models.User, error)
|
||||
GetByResetToken(string) (*models.User, error)
|
||||
GetAll() ([]*models.User, error)
|
||||
GetMany([]string) ([]*models.User, error)
|
||||
GetAllByReports(bool) ([]*models.User, error)
|
||||
GetAllByLeaderboard(bool) ([]*models.User, error)
|
||||
GetByLoggedInAfter(time.Time) ([]*models.User, error)
|
||||
GetByLastActiveAfter(time.Time) ([]*models.User, error)
|
||||
Count() (int64, error)
|
||||
@ -82,3 +86,13 @@ type IUserRepository interface {
|
||||
UpdateField(*models.User, string, interface{}) (*models.User, error)
|
||||
Delete(*models.User) error
|
||||
}
|
||||
|
||||
type ILeaderboardRepository interface {
|
||||
InsertBatch([]*models.LeaderboardItem) error
|
||||
CountAllByUser(string) (int64, error)
|
||||
CountUsers() (int64, error)
|
||||
DeleteByUser(string) error
|
||||
DeleteByUserAndInterval(string, *models.IntervalKey) error
|
||||
GetAllAggregatedByInterval(*models.IntervalKey, *uint8, int, int) ([]*models.LeaderboardItemRanked, error)
|
||||
GetAggregatedByUserAndInterval(string, *models.IntervalKey, *uint8, int, int) ([]*models.LeaderboardItemRanked, error)
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -18,15 +20,15 @@ func (r *SummaryRepository) GetAll() ([]*models.Summary, error) {
|
||||
var summaries []*models.Summary
|
||||
if err := r.db.
|
||||
Order("from_time asc").
|
||||
Preload("Projects", "type = ?", models.SummaryProject).
|
||||
Preload("Languages", "type = ?", models.SummaryLanguage).
|
||||
Preload("Editors", "type = ?", models.SummaryEditor).
|
||||
Preload("OperatingSystems", "type = ?", models.SummaryOS).
|
||||
Preload("Machines", "type = ?", models.SummaryMachine).
|
||||
// branch summaries are currently not persisted, as only relevant in combination with project filter
|
||||
Find(&summaries).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.populateItems(summaries, []clause.Interface{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
@ -39,20 +41,29 @@ func (r *SummaryRepository) Insert(summary *models.Summary) error {
|
||||
|
||||
func (r *SummaryRepository) GetByUserWithin(user *models.User, from, to time.Time) ([]*models.Summary, error) {
|
||||
var summaries []*models.Summary
|
||||
if err := r.db.
|
||||
Where(&models.Summary{UserID: user.ID}).
|
||||
Where("from_time >= ?", from.Local()).
|
||||
Where("to_time <= ?", to.Local()).
|
||||
Order("from_time asc").
|
||||
Preload("Projects", "type = ?", models.SummaryProject).
|
||||
Preload("Languages", "type = ?", models.SummaryLanguage).
|
||||
Preload("Editors", "type = ?", models.SummaryEditor).
|
||||
Preload("OperatingSystems", "type = ?", models.SummaryOS).
|
||||
Preload("Machines", "type = ?", models.SummaryMachine).
|
||||
// branch summaries are currently not persisted, as only relevant in combination with project filter
|
||||
Find(&summaries).Error; err != nil {
|
||||
|
||||
queryConditions := []clause.Interface{
|
||||
clause.Where{Exprs: r.db.Statement.BuildCondition("user_id = ?", user.ID)},
|
||||
clause.Where{Exprs: r.db.Statement.BuildCondition("from_time >= ?", from.Local())},
|
||||
clause.Where{Exprs: r.db.Statement.BuildCondition("to_time <= ?", to.Local())},
|
||||
}
|
||||
|
||||
q := r.db.Model(&models.Summary{}).
|
||||
Order("from_time asc")
|
||||
|
||||
for _, c := range queryConditions {
|
||||
q.Statement.AddClause(c)
|
||||
}
|
||||
|
||||
// branch summaries are currently not persisted, as only relevant in combination with project filter
|
||||
if err := q.Find(&summaries).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.populateItems(summaries, queryConditions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
@ -74,3 +85,46 @@ func (r *SummaryRepository) DeleteByUser(userId string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SummaryRepository) DeleteByUserBefore(userId string, t time.Time) error {
|
||||
if err := r.db.
|
||||
Where("user_id = ?", userId).
|
||||
Where("to_time <= ?", t.Local()).
|
||||
Delete(models.Summary{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// inplace
|
||||
func (r *SummaryRepository) populateItems(summaries []*models.Summary, conditions []clause.Interface) error {
|
||||
var items []*models.SummaryItem
|
||||
|
||||
summaryMap := slice.GroupWith[*models.Summary, uint](summaries, func(s *models.Summary) uint {
|
||||
return s.ID
|
||||
})
|
||||
|
||||
q := r.db.Model(&models.SummaryItem{}).
|
||||
Select("summary_items.*").
|
||||
Joins("cross join summaries").
|
||||
Where("summary_items.summary_id = summaries.id").
|
||||
Where("num_heartbeats > ?", 0)
|
||||
|
||||
for _, c := range conditions {
|
||||
q.Statement.AddClause(c)
|
||||
}
|
||||
|
||||
if err := q.Find(&items).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if _, ok := summaryMap[item.SummaryID]; !ok {
|
||||
continue
|
||||
}
|
||||
l := summaryMap[item.SummaryID][0].ItemsByType(item.Type)
|
||||
*l = append(*l, item)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -15,9 +15,9 @@ func NewUserRepository(db *gorm.DB) *UserRepository {
|
||||
return &UserRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetById(userId string) (*models.User, error) {
|
||||
func (r *UserRepository) FindOne(attributes models.User) (*models.User, error) {
|
||||
u := &models.User{}
|
||||
if err := r.db.Where(&models.User{ID: userId}).First(u).Error; err != nil {
|
||||
if err := r.db.Where(&attributes).First(u).Error; err != nil {
|
||||
return u, err
|
||||
}
|
||||
return u, nil
|
||||
@ -34,39 +34,6 @@ func (r *UserRepository) GetByIds(userIds []string) ([]*models.User, error) {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByApiKey(key string) (*models.User, error) {
|
||||
if key == "" {
|
||||
return nil, errors.New("invalid input")
|
||||
}
|
||||
u := &models.User{}
|
||||
if err := r.db.Where(&models.User{ApiKey: key}).First(u).Error; err != nil {
|
||||
return u, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByResetToken(resetToken string) (*models.User, error) {
|
||||
if resetToken == "" {
|
||||
return nil, errors.New("invalid input")
|
||||
}
|
||||
u := &models.User{}
|
||||
if err := r.db.Where(&models.User{ResetToken: resetToken}).First(u).Error; err != nil {
|
||||
return u, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByEmail(email string) (*models.User, error) {
|
||||
if email == "" {
|
||||
return nil, errors.New("invalid input")
|
||||
}
|
||||
u := &models.User{}
|
||||
if err := r.db.Where(&models.User{Email: email}).First(u).Error; err != nil {
|
||||
return u, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetAll() ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := r.db.
|
||||
@ -77,6 +44,17 @@ func (r *UserRepository) GetAll() ([]*models.User, error) {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetMany(ids []string) ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := r.db.
|
||||
Table("users").
|
||||
Where("id in ?", ids).
|
||||
Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetAllByReports(reportsEnabled bool) ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := r.db.Where(&models.User{ReportsWeekly: reportsEnabled}).Find(&users).Error; err != nil {
|
||||
@ -85,6 +63,14 @@ func (r *UserRepository) GetAllByReports(reportsEnabled bool) ([]*models.User, e
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetAllByLeaderboard(leaderboardEnabled bool) ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := r.db.Where(&models.User{PublicLeaderboard: leaderboardEnabled}).Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByLoggedInAfter(t time.Time) ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := r.db.
|
||||
@ -98,10 +84,9 @@ func (r *UserRepository) GetByLoggedInAfter(t time.Time) ([]*models.User, error)
|
||||
// Returns a list of user ids, whose last heartbeat is not older than t
|
||||
// NOTE: Only ID field will be populated
|
||||
func (r *UserRepository) GetByLastActiveAfter(t time.Time) ([]*models.User, error) {
|
||||
subQuery1 := r.db.Model(&models.User{}).
|
||||
Select("users.id as user, max(time) as time").
|
||||
Joins("left join heartbeats on users.id = heartbeats.user_id").
|
||||
Group("user")
|
||||
subQuery1 := r.db.Model(&models.Heartbeat{}).
|
||||
Select("user_id as user, max(time) as time").
|
||||
Group("user_id")
|
||||
|
||||
var userIds []string
|
||||
if err := r.db.
|
||||
@ -126,7 +111,7 @@ func (r *UserRepository) Count() (int64, error) {
|
||||
}
|
||||
|
||||
func (r *UserRepository) InsertOrGet(user *models.User) (*models.User, bool, error) {
|
||||
if u, err := r.GetById(user.ID); err == nil && u != nil && u.ID != "" {
|
||||
if u, err := r.FindOne(models.User{ID: user.ID}); err == nil && u != nil && u.ID != "" {
|
||||
return u, false, nil
|
||||
}
|
||||
|
||||
@ -157,6 +142,9 @@ func (r *UserRepository) Update(user *models.User) (*models.User, error) {
|
||||
"reset_token": user.ResetToken,
|
||||
"location": user.Location,
|
||||
"reports_weekly": user.ReportsWeekly,
|
||||
"public_leaderboard": user.PublicLeaderboard,
|
||||
"subscribed_until": user.SubscribedUntil,
|
||||
"stripe_customer_id": user.StripeCustomerId,
|
||||
}
|
||||
|
||||
result := r.db.Model(user).Updates(updateMap)
|
||||
|
@ -5,7 +5,9 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AvatarHandler struct {
|
||||
@ -33,8 +35,12 @@ func (h *AvatarHandler) RegisterRoutes(router *mux.Router) {
|
||||
func (h *AvatarHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
hash := mux.Vars(r)["hash"]
|
||||
|
||||
if utils.IsNoCache(r, 1*time.Hour) {
|
||||
h.cache.Remove(hash)
|
||||
}
|
||||
|
||||
if !h.cache.Contains(hash) {
|
||||
h.cache.Add(hash, avatars.MakeMaleAvatar(hash))
|
||||
h.cache.Add(hash, avatars.MakeAvatar(hash))
|
||||
}
|
||||
data, _ := h.cache.Get(hash)
|
||||
|
||||
|
98
routes/api/badge.go
Normal file
98
routes/api/badge.go
Normal file
@ -0,0 +1,98 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/maputil"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/gorilla/mux"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
v1 "github.com/muety/wakapi/models/compat/shields/v1"
|
||||
routeutils "github.com/muety/wakapi/routes/utils"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"github.com/narqo/go-badge"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BadgeHandler struct {
|
||||
config *conf.Config
|
||||
cache *cache.Cache
|
||||
userSrvc services.IUserService
|
||||
summarySrvc services.ISummaryService
|
||||
}
|
||||
|
||||
func NewBadgeHandler(userService services.IUserService, summaryService services.ISummaryService) *BadgeHandler {
|
||||
return &BadgeHandler{
|
||||
config: conf.Get(),
|
||||
cache: cache.New(time.Hour, time.Hour),
|
||||
userSrvc: userService,
|
||||
summarySrvc: summaryService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BadgeHandler) RegisterRoutes(router *mux.Router) {
|
||||
r := router.PathPrefix("/badge/{user}").Subrouter()
|
||||
r.Methods(http.MethodGet).HandlerFunc(h.Get)
|
||||
}
|
||||
|
||||
func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := h.userSrvc.GetUserById(mux.Vars(r)["user"])
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
interval, filters, err := routeutils.GetBadgeParams(r, user)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("%s_%v_%s", user.ID, *interval.Key, filters.Hash())
|
||||
noCache := utils.IsNoCache(r, 1*time.Hour)
|
||||
if cacheResult, ok := h.cache.Get(cacheKey); ok && !noCache {
|
||||
respondSvg(w, cacheResult.([]byte))
|
||||
return
|
||||
}
|
||||
|
||||
params := &models.SummaryParams{
|
||||
From: interval.Start,
|
||||
To: interval.End,
|
||||
User: user,
|
||||
Filters: filters,
|
||||
}
|
||||
|
||||
summary, err, status := routeutils.LoadUserSummaryByParams(h.summarySrvc, params)
|
||||
if err != nil {
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
badgeData := v1.NewBadgeDataFrom(summary)
|
||||
if customLabel := r.URL.Query().Get("label"); customLabel != "" {
|
||||
badgeData.Label = customLabel
|
||||
}
|
||||
if customColor := r.URL.Query().Get("color"); customColor != "" {
|
||||
badgeData.Color = customColor
|
||||
}
|
||||
|
||||
if badgeData.Color[0:1] != "#" && !slice.Contain(maputil.Keys(badge.ColorScheme), badgeData.Color) {
|
||||
badgeData.Color = "#" + badgeData.Color
|
||||
}
|
||||
|
||||
badgeSvg, err := badge.RenderBytes(badgeData.Label, badgeData.Message, badge.Color(badgeData.Color))
|
||||
h.cache.SetDefault(cacheKey, badgeSvg)
|
||||
respondSvg(w, badgeSvg)
|
||||
}
|
||||
|
||||
func respondSvg(w http.ResponseWriter, data []byte) {
|
||||
w.Header().Set("Content-Type", "image/svg+xml")
|
||||
w.Header().Set("Cache-Control", "max-age=3600")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(data)
|
||||
}
|
@ -2,15 +2,13 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/muety/wakapi/helpers"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/services"
|
||||
)
|
||||
|
||||
type DiagnosticsApiHandler struct {
|
||||
@ -29,9 +27,6 @@ func NewDiagnosticsApiHandler(userService services.IUserService, diagnosticsServ
|
||||
|
||||
func (h *DiagnosticsApiHandler) RegisterRoutes(router *mux.Router) {
|
||||
r := router.PathPrefix("/plugins/errors").Subrouter()
|
||||
r.Use(
|
||||
middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler,
|
||||
)
|
||||
r.Path("").Methods(http.MethodPost).HandlerFunc(h.Post)
|
||||
}
|
||||
|
||||
@ -40,26 +35,17 @@ func (h *DiagnosticsApiHandler) RegisterRoutes(router *mux.Router) {
|
||||
// @Tags diagnostics
|
||||
// @Accept json
|
||||
// @Param diagnostics body models.Diagnostics true "A single diagnostics object sent by WakaTime CLI"
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 201
|
||||
// @Router /plugins/errors [post]
|
||||
func (h *DiagnosticsApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
||||
var diagnostics models.Diagnostics
|
||||
|
||||
user := middlewares.GetPrincipal(r)
|
||||
if user == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(conf.ErrUnauthorized))
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&diagnostics); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(conf.ErrBadRequest))
|
||||
conf.Log().Request(r).Error("failed to parse diagnostics for user %s - %v", err)
|
||||
return
|
||||
}
|
||||
diagnostics.UserID = user.ID
|
||||
|
||||
if _, err := h.diagnosticsSrvc.Create(&diagnostics); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
@ -68,5 +54,5 @@ func (h *DiagnosticsApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondJSON(w, r, http.StatusCreated, struct{}{})
|
||||
helpers.RespondJSON(w, r, http.StatusCreated, struct{}{})
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/helpers"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@ -79,6 +80,12 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
||||
machineName := r.Header.Get("X-Machine-Name")
|
||||
|
||||
for _, hb := range heartbeats {
|
||||
if hb == nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("invalid heartbeat object"))
|
||||
return
|
||||
}
|
||||
|
||||
hb.OperatingSystem = opSys
|
||||
hb.Editor = editor
|
||||
hb.Machine = machineName
|
||||
@ -86,7 +93,7 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
||||
hb.UserID = user.ID
|
||||
hb.UserAgent = userAgent
|
||||
|
||||
if !hb.Valid() {
|
||||
if !hb.Valid() || !hb.Timely(h.config.App.HeartbeatsMaxAge()) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("invalid heartbeat object"))
|
||||
return
|
||||
@ -114,7 +121,7 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
defer func() {}()
|
||||
|
||||
utils.RespondJSON(w, r, http.StatusCreated, constructSuccessResponse(len(heartbeats)))
|
||||
helpers.RespondJSON(w, r, http.StatusCreated, constructSuccessResponse(len(heartbeats)))
|
||||
}
|
||||
|
||||
// construct weird response format (see https://github.com/wakatime/wakatime/blob/2e636d389bf5da4e998e05d5285a96ce2c181e3d/wakatime/api.py#L288)
|
||||
|
@ -5,12 +5,13 @@ import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/gorilla/mux"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/helpers"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
"github.com/muety/wakapi/models"
|
||||
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
||||
mm "github.com/muety/wakapi/models/metrics"
|
||||
"github.com/muety/wakapi/repositories"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sort"
|
||||
@ -36,9 +37,13 @@ const (
|
||||
DescAdminTotalUsers = "Total number of registered users."
|
||||
DescAdminActiveUsers = "Number of active users."
|
||||
|
||||
DescJobQueueEnqueued = "Number of jobs currently enqueued"
|
||||
DescJobQueueTotalFinished = "Total number of processed jobs"
|
||||
|
||||
DescMemAllocTotal = "Total number of bytes allocated for heap"
|
||||
DescMemSysTotal = "Total number of bytes obtained from the OS"
|
||||
DescGoroutines = "Total number of running goroutines"
|
||||
DescDatabaseSize = "Total database size in bytes"
|
||||
)
|
||||
|
||||
type MetricsHandler struct {
|
||||
@ -47,14 +52,16 @@ type MetricsHandler struct {
|
||||
summarySrvc services.ISummaryService
|
||||
heartbeatSrvc services.IHeartbeatService
|
||||
keyValueSrvc services.IKeyValueService
|
||||
metricsRepo *repositories.MetricsRepository
|
||||
}
|
||||
|
||||
func NewMetricsHandler(userService services.IUserService, summaryService services.ISummaryService, heartbeatService services.IHeartbeatService, keyValueService services.IKeyValueService) *MetricsHandler {
|
||||
func NewMetricsHandler(userService services.IUserService, summaryService services.ISummaryService, heartbeatService services.IHeartbeatService, keyValueService services.IKeyValueService, metricsRepo *repositories.MetricsRepository) *MetricsHandler {
|
||||
return &MetricsHandler{
|
||||
userSrvc: userService,
|
||||
summarySrvc: summaryService,
|
||||
heartbeatSrvc: heartbeatService,
|
||||
keyValueSrvc: keyValueService,
|
||||
metricsRepo: metricsRepo,
|
||||
config: conf.Get(),
|
||||
}
|
||||
}
|
||||
@ -122,7 +129,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
from, to := utils.MustResolveIntervalRawTZ("today", user.TZ())
|
||||
from, to := helpers.MustResolveIntervalRawTZ("today", user.TZ())
|
||||
|
||||
summaryToday, err := h.summarySrvc.Aliased(from, to, user, h.summarySrvc.Retrieve, nil, false)
|
||||
if err != nil {
|
||||
@ -138,77 +145,77 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
||||
|
||||
// User Metrics
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_cumulative_seconds_total",
|
||||
Desc: DescAllTime,
|
||||
Value: int(v1.NewAllTimeFrom(summaryAllTime).Data.TotalSeconds),
|
||||
Value: int64(v1.NewAllTimeFrom(summaryAllTime).Data.TotalSeconds),
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_seconds_total",
|
||||
Desc: DescTotal,
|
||||
Value: int(summaryToday.TotalTime().Seconds()),
|
||||
Value: int64(summaryToday.TotalTime().Seconds()),
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_heartbeats_total",
|
||||
Desc: DescHeartbeats,
|
||||
Value: int(heartbeatCount),
|
||||
Value: int64(heartbeatCount),
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
for _, p := range summaryToday.Projects {
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_project_seconds_total",
|
||||
Desc: DescProjects,
|
||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryProject, p.Key).Seconds()),
|
||||
Value: int64(summaryToday.TotalTimeByKey(models.SummaryProject, p.Key).Seconds()),
|
||||
Labels: []mm.Label{{Key: "name", Value: p.Key}},
|
||||
})
|
||||
}
|
||||
|
||||
for _, l := range summaryToday.Languages {
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_language_seconds_total",
|
||||
Desc: DescLanguages,
|
||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryLanguage, l.Key).Seconds()),
|
||||
Value: int64(summaryToday.TotalTimeByKey(models.SummaryLanguage, l.Key).Seconds()),
|
||||
Labels: []mm.Label{{Key: "name", Value: l.Key}},
|
||||
})
|
||||
}
|
||||
|
||||
for _, e := range summaryToday.Editors {
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_editor_seconds_total",
|
||||
Desc: DescEditors,
|
||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryEditor, e.Key).Seconds()),
|
||||
Value: int64(summaryToday.TotalTimeByKey(models.SummaryEditor, e.Key).Seconds()),
|
||||
Labels: []mm.Label{{Key: "name", Value: e.Key}},
|
||||
})
|
||||
}
|
||||
|
||||
for _, o := range summaryToday.OperatingSystems {
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_operating_system_seconds_total",
|
||||
Desc: DescOperatingSystems,
|
||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryOS, o.Key).Seconds()),
|
||||
Value: int64(summaryToday.TotalTimeByKey(models.SummaryOS, o.Key).Seconds()),
|
||||
Labels: []mm.Label{{Key: "name", Value: o.Key}},
|
||||
})
|
||||
}
|
||||
|
||||
for _, m := range summaryToday.Machines {
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_machine_seconds_total",
|
||||
Desc: DescMachines,
|
||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryMachine, m.Key).Seconds()),
|
||||
Value: int64(summaryToday.TotalTimeByKey(models.SummaryMachine, m.Key).Seconds()),
|
||||
Labels: []mm.Label{{Key: "name", Value: m.Key}},
|
||||
})
|
||||
}
|
||||
|
||||
for _, m := range summaryToday.Labels {
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_label_seconds_total",
|
||||
Desc: DescLabels,
|
||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryLabel, m.Key).Seconds()),
|
||||
Value: int64(summaryToday.TotalTimeByKey(models.SummaryLabel, m.Key).Seconds()),
|
||||
Labels: []mm.Label{{Key: "name", Value: m.Key}},
|
||||
})
|
||||
}
|
||||
@ -217,27 +224,57 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_goroutines_total",
|
||||
Desc: DescGoroutines,
|
||||
Value: runtime.NumGoroutine(),
|
||||
Value: int64(runtime.NumGoroutine()),
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_mem_alloc_total",
|
||||
Desc: DescMemAllocTotal,
|
||||
Value: int(memStats.Alloc),
|
||||
Value: int64(memStats.Alloc),
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_mem_sys_total",
|
||||
Desc: DescMemSysTotal,
|
||||
Value: int(memStats.Sys),
|
||||
Value: int64(memStats.Sys),
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
// Database metrics
|
||||
dbSize, err := h.metricsRepo.GetDatabaseSize()
|
||||
if err != nil {
|
||||
logbuch.Warn("failed to get database size (%v)", err)
|
||||
}
|
||||
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_db_total_bytes",
|
||||
Desc: DescDatabaseSize,
|
||||
Value: dbSize,
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
// Miscellaneous
|
||||
for _, qm := range conf.GetQueueMetrics() {
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_queue_jobs_enqueued",
|
||||
Value: int64(qm.EnqueuedJobs),
|
||||
Desc: DescJobQueueEnqueued,
|
||||
Labels: []mm.Label{{Key: "queue", Value: qm.Queue}},
|
||||
})
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
Name: MetricsPrefix + "_queue_jobs_total_finished",
|
||||
Value: int64(qm.FinishedJobs),
|
||||
Desc: DescJobQueueTotalFinished,
|
||||
Labels: []mm.Label{{Key: "queue", Value: qm.Queue}},
|
||||
})
|
||||
}
|
||||
|
||||
return &metrics, nil
|
||||
}
|
||||
|
||||
@ -256,7 +293,7 @@ func (h *MetricsHandler) getAdminMetrics(user *models.User) (*mm.Metrics, error)
|
||||
}
|
||||
|
||||
totalUsers, _ := h.userSrvc.Count()
|
||||
totalHeartbeats, _ := h.heartbeatSrvc.Count()
|
||||
totalHeartbeats, _ := h.heartbeatSrvc.Count(true)
|
||||
|
||||
activeUsers, err := h.userSrvc.GetActive(false)
|
||||
if err != nil {
|
||||
@ -264,31 +301,31 @@ func (h *MetricsHandler) getAdminMetrics(user *models.User) (*mm.Metrics, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_admin_seconds_total",
|
||||
Desc: DescAdminTotalTime,
|
||||
Value: totalSeconds,
|
||||
Value: int64(totalSeconds),
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_admin_heartbeats_total",
|
||||
Desc: DescAdminTotalHeartbeats,
|
||||
Value: int(totalHeartbeats),
|
||||
Value: totalHeartbeats,
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_admin_users_total",
|
||||
Desc: DescAdminTotalUsers,
|
||||
Value: int(totalUsers),
|
||||
Value: totalUsers,
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_admin_users_active_total",
|
||||
Desc: DescAdminActiveUsers,
|
||||
Value: len(activeUsers),
|
||||
Value: int64(len(activeUsers)),
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
@ -301,10 +338,10 @@ func (h *MetricsHandler) getAdminMetrics(user *models.User) (*mm.Metrics, error)
|
||||
}
|
||||
|
||||
for _, uc := range userCounts {
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
metrics = append(metrics, &mm.GaugeMetric{
|
||||
Name: MetricsPrefix + "_admin_user_heartbeats_total",
|
||||
Desc: DescAdminUserHeartbeats,
|
||||
Value: int(uc.Count),
|
||||
Value: uc.Count,
|
||||
Labels: []mm.Label{{Key: "user", Value: uc.User}},
|
||||
})
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/helpers"
|
||||
routeutils "github.com/muety/wakapi/routes/utils"
|
||||
"net/http"
|
||||
|
||||
@ -8,7 +9,6 @@ import (
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
)
|
||||
|
||||
type SummaryApiHandler struct {
|
||||
@ -37,7 +37,7 @@ func (h *SummaryApiHandler) RegisterRoutes(router *mux.Router) {
|
||||
// @ID get-summary
|
||||
// @Tags summary
|
||||
// @Produce json
|
||||
// @Param interval query string false "Interval identifier" Enums(today, yesterday, week, month, year, 7_days, last_7_days, 30_days, last_30_days, 12_months, last_12_months, any)
|
||||
// @Param interval query string false "Interval identifier" Enums(today, yesterday, week, month, year, 7_days, last_7_days, 30_days, last_30_days, 6_months, last_6_months, 12_months, last_12_months, last_year, any, all_time)
|
||||
// @Param from query string false "Start date (e.g. '2021-02-07')"
|
||||
// @Param to query string false "End date (e.g. '2021-02-08')"
|
||||
// @Param recompute query bool false "Whether to recompute the summary from raw heartbeat or use cache"
|
||||
@ -58,5 +58,5 @@ func (h *SummaryApiHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondJSON(w, r, http.StatusOK, summary)
|
||||
helpers.RespondJSON(w, r, http.StatusOK, summary)
|
||||
}
|
||||
|
@ -2,8 +2,9 @@ package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/muety/wakapi/helpers"
|
||||
routeutils "github.com/muety/wakapi/routes/utils"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@ -11,15 +12,9 @@ import (
|
||||
"github.com/muety/wakapi/models"
|
||||
v1 "github.com/muety/wakapi/models/compat/shields/v1"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
const (
|
||||
intervalPattern = `interval:([a-z0-9_]+)`
|
||||
entityFilterPattern = `(project|os|editor|language|machine|label):([_a-zA-Z0-9-\s\.]+)`
|
||||
)
|
||||
|
||||
type BadgeHandler struct {
|
||||
config *conf.Config
|
||||
userSrvc services.IUserService
|
||||
@ -48,82 +43,38 @@ func (h *BadgeHandler) RegisterRoutes(router *mux.Router) {
|
||||
// @Tags badges
|
||||
// @Produce json
|
||||
// @Param user path string true "User ID to fetch data for"
|
||||
// @Param interval path string true "Interval to aggregate data for" Enums(today, yesterday, week, month, year, 7_days, last_7_days, 30_days, last_30_days, 12_months, last_12_months, any)
|
||||
// @Param interval path string true "Interval to aggregate data for" Enums(today, yesterday, week, month, year, 7_days, last_7_days, 30_days, last_30_days, 6_months, last_6_months, 12_months, last_12_months, last_year, any, all_time)
|
||||
// @Param filter path string true "Filter to apply (e.g. 'project:wakapi' or 'language:Go')"
|
||||
// @Success 200 {object} v1.BadgeData
|
||||
// @Router /compat/shields/v1/{user}/{interval}/{filter} [get]
|
||||
func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
intervalReg := regexp.MustCompile(intervalPattern)
|
||||
entityFilterReg := regexp.MustCompile(entityFilterPattern)
|
||||
|
||||
var filterEntity, filterKey string
|
||||
if groups := entityFilterReg.FindStringSubmatch(r.URL.Path); len(groups) > 2 {
|
||||
filterEntity, filterKey = groups[1], groups[2]
|
||||
}
|
||||
|
||||
var interval = models.IntervalPast30Days
|
||||
if groups := intervalReg.FindStringSubmatch(r.URL.Path); len(groups) > 1 {
|
||||
if i, err := utils.ParseInterval(groups[1]); err == nil {
|
||||
interval = i
|
||||
}
|
||||
}
|
||||
|
||||
requestedUserId := mux.Vars(r)["user"]
|
||||
user, err := h.userSrvc.GetUserById(requestedUserId)
|
||||
user, err := h.userSrvc.GetUserById(mux.Vars(r)["user"])
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
_, rangeFrom, rangeTo := utils.ResolveIntervalTZ(interval, user.TZ())
|
||||
minStart := rangeTo.Add(-24 * time.Hour * time.Duration(user.ShareDataMaxDays))
|
||||
// negative value means no limit
|
||||
if rangeFrom.Before(minStart) && user.ShareDataMaxDays >= 0 {
|
||||
interval, filters, err := routeutils.GetBadgeParams(r, user)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("requested time range too broad"))
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var permitEntity bool
|
||||
var filters *models.Filters
|
||||
switch filterEntity {
|
||||
case "project":
|
||||
permitEntity = user.ShareProjects
|
||||
filters = models.NewFiltersWith(models.SummaryProject, filterKey)
|
||||
case "os":
|
||||
permitEntity = user.ShareOSs
|
||||
filters = models.NewFiltersWith(models.SummaryOS, filterKey)
|
||||
case "editor":
|
||||
permitEntity = user.ShareEditors
|
||||
filters = models.NewFiltersWith(models.SummaryEditor, filterKey)
|
||||
case "language":
|
||||
permitEntity = user.ShareLanguages
|
||||
filters = models.NewFiltersWith(models.SummaryLanguage, filterKey)
|
||||
case "machine":
|
||||
permitEntity = user.ShareMachines
|
||||
filters = models.NewFiltersWith(models.SummaryMachine, filterKey)
|
||||
case "label":
|
||||
permitEntity = user.ShareLabels
|
||||
filters = models.NewFiltersWith(models.SummaryLabel, filterKey)
|
||||
// branches are intentionally omitted here, as only relevant in combination with a project filter
|
||||
default:
|
||||
permitEntity = true
|
||||
filters = &models.Filters{}
|
||||
}
|
||||
|
||||
if !permitEntity {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("user did not opt in to share entity-specific data"))
|
||||
return
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("%s_%v_%s_%s", user.ID, *interval, filterEntity, filterKey)
|
||||
cacheKey := fmt.Sprintf("%s_%v_%s", user.ID, *interval.Key, filters.Hash())
|
||||
if cacheResult, ok := h.cache.Get(cacheKey); ok {
|
||||
utils.RespondJSON(w, r, http.StatusOK, cacheResult.(*v1.BadgeData))
|
||||
helpers.RespondJSON(w, r, http.StatusOK, cacheResult.(*v1.BadgeData))
|
||||
return
|
||||
}
|
||||
|
||||
summary, err, status := h.loadUserSummary(user, interval, filters)
|
||||
params := &models.SummaryParams{
|
||||
From: interval.Start,
|
||||
To: interval.End,
|
||||
User: user,
|
||||
Filters: filters,
|
||||
}
|
||||
|
||||
summary, err, status := routeutils.LoadUserSummaryByParams(h.summarySrvc, params)
|
||||
if err != nil {
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(err.Error()))
|
||||
@ -132,11 +83,11 @@ func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
vm := v1.NewBadgeDataFrom(summary)
|
||||
h.cache.SetDefault(cacheKey, vm)
|
||||
utils.RespondJSON(w, r, http.StatusOK, vm)
|
||||
helpers.RespondJSON(w, r, http.StatusOK, vm)
|
||||
}
|
||||
|
||||
func (h *BadgeHandler) loadUserSummary(user *models.User, interval *models.IntervalKey, filters *models.Filters) (*models.Summary, error, int) {
|
||||
err, from, to := utils.ResolveIntervalTZ(interval, user.TZ())
|
||||
err, from, to := helpers.ResolveIntervalTZ(interval, user.TZ())
|
||||
if err != nil {
|
||||
return nil, err, http.StatusBadRequest
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ func TestBadgeHandler_EntityPattern(t *testing.T) {
|
||||
{test: pathPrefix + "project:Anchr-Android_v2.0", key: "project", val: "Anchr-Android_v2.0"}, // all the way
|
||||
}
|
||||
|
||||
sut := regexp.MustCompile(entityFilterPattern)
|
||||
sut := regexp.MustCompile(`(project|os|editor|language|machine|label):([^:?&/]+)`) // see entityFilterPattern in badge_utils.go
|
||||
|
||||
for _, tc := range tests {
|
||||
var key, val string
|
||||
|
@ -3,12 +3,12 @@ package v1
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/helpers"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
"github.com/muety/wakapi/models"
|
||||
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
||||
routeutils "github.com/muety/wakapi/routes/utils"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
@ -50,7 +50,7 @@ func (h *AllTimeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
return // response was already sent by util function
|
||||
}
|
||||
|
||||
summary, err, status := h.loadUserSummary(user, utils.ParseSummaryFilters(r))
|
||||
summary, err, status := h.loadUserSummary(user, helpers.ParseSummaryFilters(r))
|
||||
if err != nil {
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(err.Error()))
|
||||
@ -58,7 +58,7 @@ func (h *AllTimeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
vm := v1.NewAllTimeFrom(summary)
|
||||
utils.RespondJSON(w, r, http.StatusOK, vm)
|
||||
helpers.RespondJSON(w, r, http.StatusOK, vm)
|
||||
}
|
||||
|
||||
func (h *AllTimeHandler) loadUserSummary(user *models.User, filters *models.Filters) (*models.Summary, error, int) {
|
||||
|
@ -1,6 +1,8 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/datetime"
|
||||
"github.com/muety/wakapi/helpers"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@ -10,7 +12,6 @@ import (
|
||||
wakatime "github.com/muety/wakapi/models/compat/wakatime/v1"
|
||||
routeutils "github.com/muety/wakapi/routes/utils"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
)
|
||||
|
||||
type HeartbeatsResult struct {
|
||||
@ -65,7 +66,7 @@ func (h *HeartbeatHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
timezone := user.TZ()
|
||||
rangeFrom, rangeTo := utils.StartOfDay(date.In(timezone)), utils.EndOfDay(date.In(timezone))
|
||||
rangeFrom, rangeTo := datetime.BeginOfDay(date.In(timezone)), datetime.EndOfDay(date.In(timezone))
|
||||
|
||||
heartbeats, err := h.heartbeatSrvc.GetAllWithin(rangeFrom, rangeTo, user)
|
||||
if err != nil {
|
||||
@ -81,5 +82,5 @@ func (h *HeartbeatHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
End: rangeTo.UTC().Format(time.RFC3339),
|
||||
Timezone: timezone.String(),
|
||||
}
|
||||
utils.RespondJSON(w, r, http.StatusOK, res)
|
||||
helpers.RespondJSON(w, r, http.StatusOK, res)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/helpers"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@ -11,7 +12,6 @@ import (
|
||||
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
||||
routeutils "github.com/muety/wakapi/routes/utils"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
)
|
||||
|
||||
type ProjectsHandler struct {
|
||||
@ -70,5 +70,5 @@ func (h *ProjectsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
vm := &v1.ProjectsViewModel{Data: projects}
|
||||
utils.RespondJSON(w, r, http.StatusOK, vm)
|
||||
helpers.RespondJSON(w, r, http.StatusOK, vm)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/helpers"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@ -10,7 +11,6 @@ import (
|
||||
"github.com/muety/wakapi/models"
|
||||
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
)
|
||||
|
||||
type StatsHandler struct {
|
||||
@ -48,7 +48,7 @@ func (h *StatsHandler) RegisterRoutes(router *mux.Router) {
|
||||
// @Tags wakatime
|
||||
// @Produce json
|
||||
// @Param user path string true "User ID to fetch data for (or 'current')"
|
||||
// @Param range path string false "Range interval identifier" Enums(today, yesterday, week, month, year, 7_days, last_7_days, 30_days, last_30_days, 12_months, last_12_months, any)
|
||||
// @Param range path string false "Range interval identifier" Enums(today, yesterday, week, month, year, 7_days, last_7_days, 30_days, last_30_days, 6_months, last_6_months, 12_months, last_12_months, last_year, any, all_time)
|
||||
// @Param project query string false "Project to filter by"
|
||||
// @Param language query string false "Language to filter by"
|
||||
// @Param editor query string false "Editor to filter by"
|
||||
@ -79,14 +79,14 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
rangeParam = (*models.IntervalPast7Days)[0]
|
||||
}
|
||||
|
||||
err, rangeFrom, rangeTo := utils.ResolveIntervalRawTZ(rangeParam, requestedUser.TZ())
|
||||
err, rangeFrom, rangeTo := helpers.ResolveIntervalRawTZ(rangeParam, requestedUser.TZ())
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("invalid range"))
|
||||
return
|
||||
}
|
||||
|
||||
minStart := rangeTo.Add(-24 * time.Hour * time.Duration(requestedUser.ShareDataMaxDays))
|
||||
minStart := rangeTo.AddDate(0, 0, -requestedUser.ShareDataMaxDays)
|
||||
if (authorizedUser == nil || requestedUser.ID != authorizedUser.ID) &&
|
||||
rangeFrom.Before(minStart) && requestedUser.ShareDataMaxDays >= 0 {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
@ -94,7 +94,7 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
summary, err, status := h.loadUserSummary(requestedUser, rangeFrom, rangeTo, utils.ParseSummaryFilters(r))
|
||||
summary, err, status := h.loadUserSummary(requestedUser, rangeFrom, rangeTo, helpers.ParseSummaryFilters(r))
|
||||
if err != nil {
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(err.Error()))
|
||||
@ -120,7 +120,7 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
stats.Data.Machines = nil
|
||||
}
|
||||
|
||||
utils.RespondJSON(w, r, http.StatusOK, stats)
|
||||
helpers.RespondJSON(w, r, http.StatusOK, stats)
|
||||
}
|
||||
|
||||
func (h *StatsHandler) loadUserSummary(user *models.User, start, end time.Time, filters *models.Filters) (*models.Summary, error, int) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/helpers"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@ -11,7 +12,6 @@ import (
|
||||
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
||||
routeutils "github.com/muety/wakapi/routes/utils"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
)
|
||||
|
||||
type StatusBarViewModel struct {
|
||||
@ -65,7 +65,7 @@ func (h *StatusBarHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
rangeParam = (*models.IntervalToday)[0]
|
||||
}
|
||||
|
||||
err, rangeFrom, rangeTo := utils.ResolveIntervalRawTZ(rangeParam, user.TZ())
|
||||
err, rangeFrom, rangeTo := helpers.ResolveIntervalRawTZ(rangeParam, user.TZ())
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("invalid range"))
|
||||
@ -79,7 +79,7 @@ func (h *StatusBarHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
summariesView := v1.NewSummariesFrom([]*models.Summary{summary})
|
||||
utils.RespondJSON(w, r, http.StatusOK, StatusBarViewModel{
|
||||
helpers.RespondJSON(w, r, http.StatusOK, StatusBarViewModel{
|
||||
CachedAt: time.Now(),
|
||||
Data: *summariesView.Data[0],
|
||||
})
|
||||
|
@ -2,6 +2,8 @@ package v1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/duke-git/lancet/v2/datetime"
|
||||
"github.com/muety/wakapi/helpers"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@ -49,7 +51,7 @@ func (h *SummariesHandler) RegisterRoutes(router *mux.Router) {
|
||||
// @Tags wakatime
|
||||
// @Produce json
|
||||
// @Param user path string true "User ID to fetch data for (or 'current')"
|
||||
// @Param range query string false "Range interval identifier" Enums(today, yesterday, week, month, year, 7_days, last_7_days, 30_days, last_30_days, 12_months, last_12_months, any)
|
||||
// @Param range query string false "Range interval identifier" Enums(today, yesterday, week, month, year, 7_days, last_7_days, 30_days, last_30_days, 6_months, last_6_months, 12_months, last_12_months, last_year, any, all_time)
|
||||
// @Param start query string false "Start date (e.g. '2021-02-07')"
|
||||
// @Param end query string false "End date (e.g. '2021-02-08')"
|
||||
// @Param project query string false "Project to filter by"
|
||||
@ -62,12 +64,12 @@ func (h *SummariesHandler) RegisterRoutes(router *mux.Router) {
|
||||
// @Success 200 {object} v1.SummariesViewModel
|
||||
// @Router /compat/wakatime/v1/users/{user}/summaries [get]
|
||||
func (h *SummariesHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := routeutils.CheckEffectiveUser(w, r, h.userSrvc, "current")
|
||||
user, err := routeutils.CheckEffectiveUser(w, r, h.userSrvc, "current")
|
||||
if err != nil {
|
||||
return // response was already sent by util function
|
||||
}
|
||||
|
||||
summaries, err, status := h.loadUserSummaries(r)
|
||||
summaries, err, status := h.loadUserSummaries(r, user)
|
||||
if err != nil {
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(err.Error()))
|
||||
@ -75,11 +77,10 @@ func (h *SummariesHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
vm := v1.NewSummariesFrom(summaries)
|
||||
utils.RespondJSON(w, r, http.StatusOK, vm)
|
||||
helpers.RespondJSON(w, r, http.StatusOK, vm)
|
||||
}
|
||||
|
||||
func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary, error, int) {
|
||||
user := middlewares.GetPrincipal(r)
|
||||
func (h *SummariesHandler) loadUserSummaries(r *http.Request, user *models.User) ([]*models.Summary, error, int) {
|
||||
params := r.URL.Query()
|
||||
rangeParam, startParam, endParam, tzParam := params.Get("range"), params.Get("start"), params.Get("end"), params.Get("timezone")
|
||||
|
||||
@ -93,24 +94,24 @@ func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary
|
||||
var start, end time.Time
|
||||
if rangeParam != "" {
|
||||
// range param takes precedence
|
||||
if err, parsedFrom, parsedTo := utils.ResolveIntervalRawTZ(rangeParam, timezone); err == nil {
|
||||
if err, parsedFrom, parsedTo := helpers.ResolveIntervalRawTZ(rangeParam, timezone); err == nil {
|
||||
start, end = parsedFrom, parsedTo
|
||||
} else {
|
||||
return nil, errors.New("invalid 'range' parameter"), http.StatusBadRequest
|
||||
}
|
||||
} else if err, parsedFrom, parsedTo := utils.ResolveIntervalRawTZ(startParam, timezone); err == nil && startParam == endParam {
|
||||
} else if err, parsedFrom, parsedTo := helpers.ResolveIntervalRawTZ(startParam, timezone); err == nil && startParam == endParam {
|
||||
// also accept start param to be a range param
|
||||
start, end = parsedFrom, parsedTo
|
||||
} else {
|
||||
// eventually, consider start and end params a date
|
||||
var err error
|
||||
|
||||
start, err = utils.ParseDateTimeTZ(strings.Replace(startParam, " ", "+", 1), timezone)
|
||||
start, err = helpers.ParseDateTimeTZ(strings.Replace(startParam, " ", "+", 1), timezone)
|
||||
if err != nil {
|
||||
return nil, errors.New("missing required 'start' parameter"), http.StatusBadRequest
|
||||
}
|
||||
|
||||
end, err = utils.ParseDateTimeTZ(strings.Replace(endParam, " ", "+", 1), timezone)
|
||||
end, err = helpers.ParseDateTimeTZ(strings.Replace(endParam, " ", "+", 1), timezone)
|
||||
if err != nil {
|
||||
return nil, errors.New("missing required 'end' parameter"), http.StatusBadRequest
|
||||
}
|
||||
@ -120,7 +121,7 @@ func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary
|
||||
// i.e. for wakatime, an interval 2021-04-29 - 2021-04-29 is actually 2021-04-29 - 2021-04-30,
|
||||
// while for wakapi it would be empty
|
||||
// see https://github.com/muety/wakapi/issues/192
|
||||
end = utils.EndOfDay(end).Add(-1 * time.Second)
|
||||
end = datetime.EndOfDay(end)
|
||||
|
||||
overallParams := &models.SummaryParams{
|
||||
From: start,
|
||||
@ -132,7 +133,7 @@ func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary
|
||||
summaries := make([]*models.Summary, len(intervals))
|
||||
|
||||
// filtering
|
||||
filters := utils.ParseSummaryFilters(r)
|
||||
filters := helpers.ParseSummaryFilters(r)
|
||||
|
||||
for i, interval := range intervals {
|
||||
summary, err := h.summarySrvc.Aliased(interval[0], interval[1], user, h.summarySrvc.Retrieve, filters, end.After(time.Now()))
|
||||
|
@ -1,6 +1,7 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/helpers"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@ -9,7 +10,6 @@ import (
|
||||
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
||||
routeutils "github.com/muety/wakapi/routes/utils"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
)
|
||||
|
||||
type UsersHandler struct {
|
||||
@ -56,5 +56,5 @@ func (h *UsersHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
conf.Log().Request(r).Error("%v", err)
|
||||
}
|
||||
|
||||
utils.RespondJSON(w, r, http.StatusOK, v1.UserViewModel{Data: user})
|
||||
helpers.RespondJSON(w, r, http.StatusOK, v1.UserViewModel{Data: user})
|
||||
}
|
||||
|
128
routes/compat/wakatime/v1/users_test.go
Normal file
128
routes/compat/wakatime/v1/users_test.go
Normal file
@ -0,0 +1,128 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
"github.com/muety/wakapi/mocks"
|
||||
"github.com/muety/wakapi/models"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
adminUser = &models.User{
|
||||
ID: "AdminUser",
|
||||
ApiKey: "admin-user-api-key",
|
||||
Email: "admin@user.com",
|
||||
IsAdmin: true,
|
||||
CreatedAt: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 1, time.UTC)),
|
||||
LastLoggedInAt: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 2, time.UTC)),
|
||||
}
|
||||
|
||||
basicUser = &models.User{
|
||||
ID: "BasicUser",
|
||||
ApiKey: "basic-user-api-key",
|
||||
Email: "basic@user.com",
|
||||
IsAdmin: false,
|
||||
CreatedAt: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 3, time.UTC)),
|
||||
LastLoggedInAt: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 4, time.UTC)),
|
||||
}
|
||||
)
|
||||
|
||||
func TestUsersHandler_Get(t *testing.T) {
|
||||
router := mux.NewRouter()
|
||||
apiRouter := router.PathPrefix("/api").Subrouter().StrictSlash(true)
|
||||
apiRouter.Use(middlewares.NewPrincipalMiddleware())
|
||||
|
||||
userServiceMock := new(mocks.UserServiceMock)
|
||||
userServiceMock.On("GetUserById", "AdminUser").Return(adminUser, nil)
|
||||
userServiceMock.On("GetUserByKey", "admin-user-api-key").Return(adminUser, nil)
|
||||
userServiceMock.On("GetUserById", "BasicUser").Return(basicUser, nil)
|
||||
userServiceMock.On("GetUserByKey", "basic-user-api-key").Return(basicUser, nil)
|
||||
|
||||
heartbeatServiceMock := new(mocks.HeartbeatServiceMock)
|
||||
heartbeatServiceMock.On("GetLatestByUser", adminUser).Return(&models.Heartbeat{
|
||||
CreatedAt: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 5, time.UTC)),
|
||||
Time: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 6, time.UTC)),
|
||||
}, nil)
|
||||
heartbeatServiceMock.On("GetLatestByUser", basicUser).Return(&models.Heartbeat{
|
||||
CreatedAt: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 5, time.UTC)),
|
||||
Time: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 6, time.UTC)),
|
||||
}, nil)
|
||||
|
||||
usersHandler := NewUsersHandler(userServiceMock, heartbeatServiceMock)
|
||||
usersHandler.RegisterRoutes(apiRouter)
|
||||
|
||||
t.Run("when requesting own user data", func(t *testing.T) {
|
||||
t.Run("should return own data", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/compat/wakatime/v1/users/AdminUser", nil)
|
||||
req.Header.Add(
|
||||
"Authorization",
|
||||
fmt.Sprintf("Bearer %s", base64.StdEncoding.EncodeToString([]byte(adminUser.ApiKey))),
|
||||
)
|
||||
requestRecorder := httptest.NewRecorder()
|
||||
apiRouter.ServeHTTP(requestRecorder, req)
|
||||
res := requestRecorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Errorf("unextected error. Error: %s", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(data), "\"username\":\"AdminUser\"") {
|
||||
t.Errorf("invalid response received. Expected json Received: %s", string(data))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("when requesting another users data", func(t *testing.T) {
|
||||
t.Run("should respond with '401 unauthorized' if not an admin user", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/compat/wakatime/v1/users/AdminUser", nil)
|
||||
req.Header.Add(
|
||||
"Authorization",
|
||||
fmt.Sprintf("Bearer %s", base64.StdEncoding.EncodeToString([]byte(basicUser.ApiKey))),
|
||||
)
|
||||
requestRecorder := httptest.NewRecorder()
|
||||
apiRouter.ServeHTTP(requestRecorder, req)
|
||||
res := requestRecorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Errorf("unextected error. Error: %s", err)
|
||||
}
|
||||
|
||||
if string(data) != "401 unauthorized" {
|
||||
t.Errorf("invalid response received. Expected: '401 unauthorized' Received: %s", string(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should receive user data if requesting user is an admin", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/compat/wakatime/v1/users/BasicUser", nil)
|
||||
req.Header.Add(
|
||||
"Authorization",
|
||||
fmt.Sprintf("Bearer %s", base64.StdEncoding.EncodeToString([]byte(adminUser.ApiKey))),
|
||||
)
|
||||
requestRecorder := httptest.NewRecorder()
|
||||
apiRouter.ServeHTTP(requestRecorder, req)
|
||||
res := requestRecorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Errorf("unextected error. Error: %s", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(data), "\"username\":\"BasicUser\"") {
|
||||
t.Errorf("invalid response received. Expected 'BasicUser' info Received: %s", string(data))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
@ -1,15 +1,19 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/schema"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/models/view"
|
||||
routeutils "github.com/muety/wakapi/routes/utils"
|
||||
"github.com/muety/wakapi/services"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -43,29 +47,36 @@ func (h *HomeHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
templates[conf.IndexTemplate].Execute(w, h.buildViewModel(r))
|
||||
templates[conf.IndexTemplate].Execute(w, h.buildViewModel(r, w))
|
||||
}
|
||||
|
||||
func (h *HomeHandler) buildViewModel(r *http.Request) *view.HomeViewModel {
|
||||
func (h *HomeHandler) buildViewModel(r *http.Request, w http.ResponseWriter) *view.HomeViewModel {
|
||||
var totalHours int
|
||||
var totalUsers int
|
||||
var newsbox view.Newsbox
|
||||
|
||||
if t, err := h.keyValueSrvc.GetString(conf.KeyLatestTotalTime); err == nil && t != nil && t.Value != "" {
|
||||
if d, err := time.ParseDuration(t.Value); err == nil {
|
||||
if kv, err := h.keyValueSrvc.GetString(conf.KeyLatestTotalTime); err == nil && kv != nil && kv.Value != "" {
|
||||
if d, err := time.ParseDuration(kv.Value); err == nil {
|
||||
totalHours = int(d.Hours())
|
||||
}
|
||||
}
|
||||
|
||||
if t, err := h.keyValueSrvc.GetString(conf.KeyLatestTotalUsers); err == nil && t != nil && t.Value != "" {
|
||||
if d, err := strconv.Atoi(t.Value); err == nil {
|
||||
if kv, err := h.keyValueSrvc.GetString(conf.KeyLatestTotalUsers); err == nil && kv != nil && kv.Value != "" {
|
||||
if d, err := strconv.Atoi(kv.Value); err == nil {
|
||||
totalUsers = d
|
||||
}
|
||||
}
|
||||
|
||||
return &view.HomeViewModel{
|
||||
Success: r.URL.Query().Get("success"),
|
||||
Error: r.URL.Query().Get("error"),
|
||||
if kv, err := h.keyValueSrvc.GetString(conf.KeyNewsbox); err == nil && kv != nil && kv.Value != "" {
|
||||
if err := json.NewDecoder(strings.NewReader(kv.Value)).Decode(&newsbox); err != nil {
|
||||
logbuch.Error("failed to decode newsbox message - %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
vm := &view.HomeViewModel{
|
||||
TotalHours: totalHours,
|
||||
TotalUsers: totalUsers,
|
||||
Newsbox: &newsbox,
|
||||
}
|
||||
return routeutils.WithSessionMessages(vm, r, w)
|
||||
}
|
||||
|
@ -39,8 +39,5 @@ func (h *ImprintHandler) GetImprint(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *ImprintHandler) buildViewModel(r *http.Request) *view.ImprintViewModel {
|
||||
return &view.ImprintViewModel{
|
||||
Success: r.URL.Query().Get("success"),
|
||||
Error: r.URL.Query().Get("error"),
|
||||
}
|
||||
return &view.ImprintViewModel{}
|
||||
}
|
||||
|
151
routes/leaderboard.go
Normal file
151
routes/leaderboard.go
Normal file
@ -0,0 +1,151 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/gorilla/mux"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/models/view"
|
||||
routeutils "github.com/muety/wakapi/routes/utils"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LeaderboardHandler struct {
|
||||
config *conf.Config
|
||||
userService services.IUserService
|
||||
leaderboardService services.ILeaderboardService
|
||||
}
|
||||
|
||||
var allowedAggregations = map[string]uint8{
|
||||
"language": models.SummaryLanguage,
|
||||
}
|
||||
|
||||
func NewLeaderboardHandler(userService services.IUserService, leaderboardService services.ILeaderboardService) *LeaderboardHandler {
|
||||
return &LeaderboardHandler{
|
||||
config: conf.Get(),
|
||||
userService: userService,
|
||||
leaderboardService: leaderboardService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *LeaderboardHandler) RegisterRoutes(router *mux.Router) {
|
||||
r := router.PathPrefix("/leaderboard").Subrouter()
|
||||
r.Use(
|
||||
middlewares.NewAuthenticateMiddleware(h.userService).
|
||||
WithRedirectTarget(defaultErrorRedirectTarget()).
|
||||
WithRedirectErrorMessage("unauthorized").
|
||||
WithOptionalFor([]string{"/"}).
|
||||
Handler,
|
||||
)
|
||||
r.Methods(http.MethodGet).HandlerFunc(h.GetIndex)
|
||||
}
|
||||
|
||||
func (h *LeaderboardHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||
if h.config.IsDev() {
|
||||
loadTemplates()
|
||||
}
|
||||
if err := templates[conf.LeaderboardTemplate].Execute(w, h.buildViewModel(r, w)); err != nil {
|
||||
logbuch.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (h *LeaderboardHandler) buildViewModel(r *http.Request, w http.ResponseWriter) *view.LeaderboardViewModel {
|
||||
user := middlewares.GetPrincipal(r)
|
||||
byParam := strings.ToLower(r.URL.Query().Get("by"))
|
||||
keyParam := strings.ToLower(r.URL.Query().Get("key"))
|
||||
pageParams := utils.ParsePageParamsWithDefault(r, 1, 100)
|
||||
// note: pagination is not fully implemented, yet
|
||||
// count function to get total item / total pages is missing
|
||||
// and according ui (+ optionally search bar) is missing, too
|
||||
|
||||
var err error
|
||||
var leaderboard models.Leaderboard
|
||||
var userLanguages map[string][]string
|
||||
var topKeys []string
|
||||
|
||||
if byParam == "" {
|
||||
leaderboard, err = h.leaderboardService.GetByInterval(models.IntervalPast7Days, pageParams, true)
|
||||
if err != nil {
|
||||
conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err)
|
||||
return &view.LeaderboardViewModel{
|
||||
Messages: view.Messages{Error: criticalError},
|
||||
}
|
||||
}
|
||||
|
||||
// regardless of page, always show own rank
|
||||
if user != nil && !leaderboard.HasUser(user.ID) {
|
||||
// but only if leaderboard spans multiple pages
|
||||
if count, err := h.leaderboardService.CountUsers(); err == nil && count > int64(pageParams.PageSize) {
|
||||
if l, err := h.leaderboardService.GetByIntervalAndUser(models.IntervalPast7Days, user.ID, true); err == nil && len(l) > 0 {
|
||||
leaderboard = append(leaderboard, l[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if by, ok := allowedAggregations[byParam]; ok {
|
||||
leaderboard, err = h.leaderboardService.GetAggregatedByInterval(models.IntervalPast7Days, &by, pageParams, true)
|
||||
if err != nil {
|
||||
conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err)
|
||||
return &view.LeaderboardViewModel{
|
||||
Messages: view.Messages{Error: criticalError},
|
||||
}
|
||||
}
|
||||
|
||||
// regardless of page, always show own rank
|
||||
if user != nil {
|
||||
// but only if leaderboard could, in theory, span multiple pages
|
||||
if count, err := h.leaderboardService.CountUsers(); err == nil && count > int64(pageParams.PageSize) {
|
||||
if l, err := h.leaderboardService.GetAggregatedByIntervalAndUser(models.IntervalPast7Days, user.ID, &by, true); err == nil {
|
||||
leaderboard.AddMany(l)
|
||||
} else {
|
||||
conf.Log().Request(r).Error("error while fetching own aggregated user leaderboard - %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userLeaderboards := slice.GroupWith[*models.LeaderboardItemRanked, string](leaderboard, func(item *models.LeaderboardItemRanked) string {
|
||||
return item.UserID
|
||||
})
|
||||
userLanguages = map[string][]string{}
|
||||
for u, items := range userLeaderboards {
|
||||
userLanguages[u] = models.Leaderboard(items).TopKeysByUser(models.SummaryLanguage, u)
|
||||
}
|
||||
|
||||
topKeys = leaderboard.TopKeys(by)
|
||||
if len(topKeys) > 0 {
|
||||
if keyParam == "" {
|
||||
keyParam = topKeys[0]
|
||||
}
|
||||
keyParam = strings.ToLower(keyParam)
|
||||
leaderboard = leaderboard.TopByKey(by, keyParam)
|
||||
}
|
||||
} else {
|
||||
return &view.LeaderboardViewModel{
|
||||
Messages: view.Messages{Error: fmt.Sprintf("unsupported aggregation '%s'", byParam)},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var apiKey string
|
||||
if user != nil {
|
||||
apiKey = user.ApiKey
|
||||
}
|
||||
|
||||
vm := &view.LeaderboardViewModel{
|
||||
User: user,
|
||||
By: byParam,
|
||||
Key: keyParam,
|
||||
Items: leaderboard,
|
||||
UserLanguages: userLanguages,
|
||||
TopKeys: topKeys,
|
||||
ApiKey: apiKey,
|
||||
PageParams: pageParams,
|
||||
}
|
||||
return routeutils.WithSessionMessages(vm, r, w)
|
||||
}
|
@ -5,8 +5,10 @@ import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/gorilla/mux"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/models/view"
|
||||
routeutils "github.com/muety/wakapi/routes/utils"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"net/http"
|
||||
@ -31,13 +33,21 @@ func NewLoginHandler(userService services.IUserService, mailService services.IMa
|
||||
func (h *LoginHandler) RegisterRoutes(router *mux.Router) {
|
||||
router.Path("/login").Methods(http.MethodGet).HandlerFunc(h.GetIndex)
|
||||
router.Path("/login").Methods(http.MethodPost).HandlerFunc(h.PostLogin)
|
||||
router.Path("/logout").Methods(http.MethodPost).HandlerFunc(h.PostLogout)
|
||||
router.Path("/signup").Methods(http.MethodGet).HandlerFunc(h.GetSignup)
|
||||
router.Path("/signup").Methods(http.MethodPost).HandlerFunc(h.PostSignup)
|
||||
router.Path("/set-password").Methods(http.MethodGet).HandlerFunc(h.GetSetPassword)
|
||||
router.Path("/set-password").Methods(http.MethodPost).HandlerFunc(h.PostSetPassword)
|
||||
router.Path("/reset-password").Methods(http.MethodGet).HandlerFunc(h.GetResetPassword)
|
||||
router.Path("/reset-password").Methods(http.MethodPost).HandlerFunc(h.PostResetPassword)
|
||||
|
||||
authMiddleware := middlewares.NewAuthenticateMiddleware(h.userSrvc).
|
||||
WithRedirectTarget(defaultErrorRedirectTarget()).
|
||||
WithRedirectErrorMessage("unauthorized").
|
||||
WithOptionalFor([]string{"/logout"})
|
||||
|
||||
logoutRouter := router.PathPrefix("/logout").Subrouter()
|
||||
logoutRouter.Use(authMiddleware.Handler)
|
||||
logoutRouter.Path("").Methods(http.MethodPost).HandlerFunc(h.PostLogout)
|
||||
}
|
||||
|
||||
func (h *LoginHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||
@ -50,7 +60,7 @@ func (h *LoginHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r))
|
||||
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r, w))
|
||||
}
|
||||
|
||||
func (h *LoginHandler) PostLogin(w http.ResponseWriter, r *http.Request) {
|
||||
@ -66,32 +76,33 @@ func (h *LoginHandler) PostLogin(w http.ResponseWriter, r *http.Request) {
|
||||
var login models.Login
|
||||
if err := r.ParseForm(); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r, w).WithError("missing parameters"))
|
||||
return
|
||||
}
|
||||
if err := loginDecoder.Decode(&login, r.PostForm); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r, w).WithError("missing parameters"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userSrvc.GetUserById(login.Username)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r).WithError("resource not found"))
|
||||
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r, w).WithError("resource not found"))
|
||||
return
|
||||
}
|
||||
|
||||
if !utils.CompareBcrypt(user.Password, login.Password, h.config.Security.PasswordSalt) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r).WithError("invalid credentials"))
|
||||
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r, w).WithError("invalid credentials"))
|
||||
return
|
||||
}
|
||||
|
||||
encoded, err := h.config.Security.SecureCookie.Encode(models.AuthCookieKey, login.Username)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
|
||||
conf.Log().Request(r).Error("failed to encode secure cookie - %v", err)
|
||||
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r, w).WithError("internal server error"))
|
||||
return
|
||||
}
|
||||
|
||||
@ -107,6 +118,9 @@ func (h *LoginHandler) PostLogout(w http.ResponseWriter, r *http.Request) {
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
if user := middlewares.GetPrincipal(r); user != nil {
|
||||
h.userSrvc.FlushUserCache(user.ID)
|
||||
}
|
||||
http.SetCookie(w, h.config.GetClearCookie(models.AuthCookieKey))
|
||||
http.Redirect(w, r, fmt.Sprintf("%s/", h.config.Server.BasePath), http.StatusFound)
|
||||
}
|
||||
@ -121,7 +135,7 @@ func (h *LoginHandler) GetSignup(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r))
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r, w))
|
||||
}
|
||||
|
||||
func (h *LoginHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
|
||||
@ -131,7 +145,7 @@ func (h *LoginHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if !h.config.IsDev() && !h.config.Security.AllowSignup {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("registration is disabled on this server"))
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r, w).WithError("registration is disabled on this server"))
|
||||
return
|
||||
}
|
||||
|
||||
@ -143,18 +157,18 @@ func (h *LoginHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
|
||||
var signup models.Signup
|
||||
if err := r.ParseForm(); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r, w).WithError("missing parameters"))
|
||||
return
|
||||
}
|
||||
if err := signupDecoder.Decode(&signup, r.PostForm); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r, w).WithError("missing parameters"))
|
||||
return
|
||||
}
|
||||
|
||||
if !signup.IsValid() {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("invalid parameters"))
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r, w).WithError("invalid parameters"))
|
||||
return
|
||||
}
|
||||
|
||||
@ -163,23 +177,25 @@ func (h *LoginHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
|
||||
_, created, err := h.userSrvc.CreateOrGet(&signup, numUsers == 0)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("failed to create new user"))
|
||||
conf.Log().Request(r).Error("failed to create new user - %v", err)
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r, w).WithError("failed to create new user"))
|
||||
return
|
||||
}
|
||||
if !created {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("user already existing"))
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r, w).WithError("user already existing"))
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("%s/?success=%s", h.config.Server.BasePath, "account created successfully"), http.StatusFound)
|
||||
routeutils.SetSuccess(r, w, "account created successfully")
|
||||
http.Redirect(w, r, h.config.Server.BasePath, http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *LoginHandler) GetResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
if h.config.IsDev() {
|
||||
loadTemplates()
|
||||
}
|
||||
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r))
|
||||
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r, w))
|
||||
}
|
||||
|
||||
func (h *LoginHandler) GetSetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
@ -191,12 +207,12 @@ func (h *LoginHandler) GetSetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
token := values.Get("token")
|
||||
if token == "" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("invalid or missing token"))
|
||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r, w).WithError("invalid or missing token"))
|
||||
return
|
||||
}
|
||||
|
||||
vm := &view.SetPasswordViewModel{
|
||||
LoginViewModel: *h.buildViewModel(r),
|
||||
LoginViewModel: *h.buildViewModel(r, w),
|
||||
Token: token,
|
||||
}
|
||||
|
||||
@ -211,25 +227,25 @@ func (h *LoginHandler) PostSetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
var setRequest models.SetPasswordRequest
|
||||
if err := r.ParseForm(); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r, w).WithError("missing parameters"))
|
||||
return
|
||||
}
|
||||
if err := signupDecoder.Decode(&setRequest, r.PostForm); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r, w).WithError("missing parameters"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userSrvc.GetUserByResetToken(setRequest.Token)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("invalid token"))
|
||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r, w).WithError("invalid token"))
|
||||
return
|
||||
}
|
||||
|
||||
if !setRequest.IsValid() {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("invalid parameters"))
|
||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r, w).WithError("invalid parameters"))
|
||||
return
|
||||
}
|
||||
|
||||
@ -237,7 +253,8 @@ func (h *LoginHandler) PostSetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
user.ResetToken = ""
|
||||
if hash, err := utils.HashBcrypt(user.Password, h.config.Security.PasswordSalt); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to set new password"))
|
||||
conf.Log().Request(r).Error("failed to set new password - %v", err)
|
||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r, w).WithError("failed to set new password"))
|
||||
return
|
||||
} else {
|
||||
user.Password = hash
|
||||
@ -245,11 +262,13 @@ func (h *LoginHandler) PostSetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if _, err := h.userSrvc.Update(user); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to save new password"))
|
||||
conf.Log().Request(r).Error("failed to save new password - %v", err)
|
||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r, w).WithError("failed to save new password"))
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("%s/login?success=%s", h.config.Server.BasePath, "password updated successfully"), http.StatusFound)
|
||||
routeutils.SetSuccess(r, w, "password updated successfully")
|
||||
http.Redirect(w, r, fmt.Sprintf("%s/login", h.config.Server.BasePath), http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
@ -259,26 +278,27 @@ func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
if !h.config.Mail.Enabled {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("mailing is disabled on this server"))
|
||||
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r, w).WithError("mailing is disabled on this server"))
|
||||
return
|
||||
}
|
||||
|
||||
var resetRequest models.ResetPasswordRequest
|
||||
if err := r.ParseForm(); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r, w).WithError("missing parameters"))
|
||||
return
|
||||
}
|
||||
if err := resetPasswordDecoder.Decode(&resetRequest, r.PostForm); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r, w).WithError("missing parameters"))
|
||||
return
|
||||
}
|
||||
|
||||
if user, err := h.userSrvc.GetUserByEmail(resetRequest.Email); user != nil && err == nil {
|
||||
if u, err := h.userSrvc.GenerateResetToken(user); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to generate password reset token"))
|
||||
conf.Log().Request(r).Error("failed to generate password reset token - %v", err)
|
||||
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r, w).WithError("failed to generate password reset token"))
|
||||
return
|
||||
} else {
|
||||
go func(user *models.User) {
|
||||
@ -294,16 +314,16 @@ func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request)
|
||||
conf.Log().Request(r).Warn("password reset requested for unregistered address '%s'", resetRequest.Email)
|
||||
}
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("%s/?success=%s", h.config.Server.BasePath, "an e-mail was sent to you in case your e-mail address was registered"), http.StatusFound)
|
||||
routeutils.SetSuccess(r, w, "an e-mail was sent to you in case your e-mail address was registered")
|
||||
http.Redirect(w, r, h.config.Server.BasePath, http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *LoginHandler) buildViewModel(r *http.Request) *view.LoginViewModel {
|
||||
func (h *LoginHandler) buildViewModel(r *http.Request, w http.ResponseWriter) *view.LoginViewModel {
|
||||
numUsers, _ := h.userSrvc.Count()
|
||||
|
||||
return &view.LoginViewModel{
|
||||
Success: r.URL.Query().Get("success"),
|
||||
Error: r.URL.Query().Get("error"),
|
||||
vm := &view.LoginViewModel{
|
||||
TotalUsers: int(numUsers),
|
||||
AllowSignup: h.config.IsDev() || h.config.Security.AllowSignup,
|
||||
}
|
||||
return routeutils.WithSessionMessages(vm, r, w)
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user