Compare commits
1 Commits
fix/lint-e
...
661a3ba79f
| Author | SHA1 | Date | |
|---|---|---|---|
| 661a3ba79f |
@@ -10,6 +10,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
- run: docker build . -t spend-sparrow-test
|
- run: docker build . -t spend-sparrow-test
|
||||||
- run: docker rmi spend-sparrow-test
|
- run: docker rmi spend-sparrow-test
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
- run: docker login git.wundenbergs.de -u tim -p ${{ secrets.DOCKER_GITEA_TOKEN }}
|
- run: docker login git.wundenbergs.de -u tim -p ${{ secrets.DOCKER_GITEA_TOKEN }}
|
||||||
- run: docker build . -t git.wundenbergs.de/x/spend-sparrow:latest -t git.wundenbergs.de/x/spend-sparrow:$GITHUB_SHA
|
- run: docker build . -t git.wundenbergs.de/x/spend-sparrow:latest -t git.wundenbergs.de/x/spend-sparrow:$GITHUB_SHA
|
||||||
- run: docker push git.wundenbergs.de/x/spend-sparrow:latest
|
- run: docker push git.wundenbergs.de/x/spend-sparrow:latest
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -25,13 +25,12 @@ go.work.sum
|
|||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
data/
|
*.db
|
||||||
secrets/
|
secrets/
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
static/css/tailwind.css
|
static/css/tailwind.css
|
||||||
static/js/htmx.min.js
|
static/js/htmx.min.js
|
||||||
static/js/echarts.min.js
|
|
||||||
tmp/
|
tmp/
|
||||||
|
|
||||||
mocks/*
|
mocks/*
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ linters:
|
|||||||
- cyclop
|
- cyclop
|
||||||
- contextcheck
|
- contextcheck
|
||||||
- bodyclose # i don't care in the tests, the implementation itself doesn't do http requests
|
- bodyclose # i don't care in the tests, the implementation itself doesn't do http requests
|
||||||
- wsl_v5
|
|
||||||
- noinlineerr
|
|
||||||
- unqueryvet
|
|
||||||
settings:
|
settings:
|
||||||
nestif:
|
nestif:
|
||||||
min-complexity: 6
|
min-complexity: 6
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.25.3@sha256:7e3cbcd2f6af1bebb937462ec29f77ce28b406081af509afed158fa8721f11af AS builder_go
|
FROM golang:1.24.4@sha256:db5d0afbfb4ab648af2393b92e87eaae9ad5e01132803d80caef91b5752d289c AS builder_go
|
||||||
WORKDIR /spend-sparrow
|
WORKDIR /spend-sparrow
|
||||||
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||||
RUN go install github.com/a-h/templ/cmd/templ@latest
|
RUN go install github.com/a-h/templ/cmd/templ@latest
|
||||||
@@ -13,7 +13,7 @@ RUN golangci-lint run ./...
|
|||||||
RUN go build -o /spend-sparrow/spend-sparrow .
|
RUN go build -o /spend-sparrow/spend-sparrow .
|
||||||
|
|
||||||
|
|
||||||
FROM node:24.11.0@sha256:e5bbac0e9b8a6e3b96a86a82bbbcf4c533a879694fd613ed616bae5116f6f243 AS builder_node
|
FROM node:22.16.0@sha256:0b5b940c21ab03353de9042f9166c75bcfc53c4cd0508c7fd88576646adbf875 AS builder_node
|
||||||
WORKDIR /spend-sparrow
|
WORKDIR /spend-sparrow
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm clean-install
|
RUN npm clean-install
|
||||||
@@ -21,7 +21,7 @@ COPY . ./
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
FROM debian:13.1@sha256:01a723bf5bfb21b9dda0c9a33e0538106e4d02cce8f557e118dd61259553d598
|
FROM debian:12.11@sha256:bd73076dc2cd9c88f48b5b358328f24f2a4289811bd73787c031e20db9f97123
|
||||||
WORKDIR /spend-sparrow
|
WORKDIR /spend-sparrow
|
||||||
RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env
|
RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env
|
||||||
COPY migration ./migration
|
COPY migration ./migration
|
||||||
|
|||||||
10
Readme.md
10
Readme.md
@@ -3,15 +3,6 @@
|
|||||||
|
|
||||||
SpendSparrow is a web app to keep track of expenses and income. It is very opinionated by keeping an keen eye on disciplin of it's users. Every Expense needs to be mapped to a Piggy Bank. For emergencies, funds can be moved between Piggy Banks.
|
SpendSparrow is a web app to keep track of expenses and income. It is very opinionated by keeping an keen eye on disciplin of it's users. Every Expense needs to be mapped to a Piggy Bank. For emergencies, funds can be moved between Piggy Banks.
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
|
||||||
go install github.com/a-h/templ/cmd/templ@latest
|
|
||||||
go install github.com/vektra/mockery/v2@latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Design priciples
|
## Design priciples
|
||||||
|
|
||||||
The State of the application can always be calculated on the fly. Even though it is not an Event Streaming Application, it is still important to be able to recalculate historic data.
|
The State of the application can always be calculated on the fly. Even though it is not an Event Streaming Application, it is still important to be able to recalculate historic data.
|
||||||
@@ -19,4 +10,3 @@ It may be applicable to do some sort of monthly snapshots to speed up calculatio
|
|||||||
|
|
||||||
This applications uses as little dependencies as feasible, especially on the front end.
|
This applications uses as little dependencies as feasible, especially on the front end.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="210mm"
|
|
||||||
height="297mm"
|
|
||||||
viewBox="0 0 210 297"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
|
||||||
sodipodi:docname="logo-inkscape.svg"
|
|
||||||
inkscape:export-filename="static/logo.svg"
|
|
||||||
inkscape:export-xdpi="96"
|
|
||||||
inkscape:export-ydpi="96"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#999999"
|
|
||||||
borderopacity="1"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:zoom="2.5914723"
|
|
||||||
inkscape:cx="385.10927"
|
|
||||||
inkscape:cy="275.712"
|
|
||||||
inkscape:window-width="2252"
|
|
||||||
inkscape:window-height="1450"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="layer2" />
|
|
||||||
<defs
|
|
||||||
id="defs1">
|
|
||||||
<rect
|
|
||||||
x="115.37843"
|
|
||||||
y="80.263254"
|
|
||||||
width="470.38898"
|
|
||||||
height="197.18521"
|
|
||||||
id="rect2" />
|
|
||||||
<rect
|
|
||||||
x="175.18448"
|
|
||||||
y="463.06726"
|
|
||||||
width="253.29221"
|
|
||||||
height="303.50433"
|
|
||||||
id="rect6" />
|
|
||||||
</defs>
|
|
||||||
<g
|
|
||||||
inkscape:label="Faviocon"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1">
|
|
||||||
<path
|
|
||||||
d="m 59.240389,97.978247 c 1.775354,-0.394229 4.087813,-2.156354 4.439709,-3.024187 0.206375,-0.508 -0.822855,-1.30175 -1.098021,-1.621896 -0.629709,-0.73025 -0.375709,-1.090083 -0.132292,-1.960562 0.277813,-0.989542 -0.381,-2.082271 -1.314979,-2.510896 -0.933979,-0.428625 -2.050521,-0.293688 -2.989792,0.124354 -0.939271,0.418042 -1.740958,1.090083 -2.526771,1.751542 -0.574145,-0.36248 -1.489604,-1.963209 -2.97127,-0.923396 -1.023938,0.717021 -1.116542,2.278062 -0.98425,3.52425 0.309562,2.876021 1.018645,4.368271 2.354791,4.770437 1.688042,0.508 3.556,0.240771 5.222875,-0.129646"
|
|
||||||
style="fill:#ffca28;stroke-width:0.264583"
|
|
||||||
id="path1-3"
|
|
||||||
inkscape:export-filename="static/favicon.svg"
|
|
||||||
inkscape:export-xdpi="96"
|
|
||||||
inkscape:export-ydpi="96" />
|
|
||||||
<path
|
|
||||||
d="m 62.124348,89.704727 c -0.224896,3.876145 -4.005792,6.447895 -5.799667,7.580312 l 1.164167,1.000125 c 0,0 0.738187,0.01588 1.748895,-0.306917 1.733021,-0.550333 4.265084,-2.106083 4.439709,-3.024187 0.256646,-1.336146 -1.113896,-1.045104 -1.423459,-2.100792 -0.161395,-0.558271 0.785813,-1.613958 -0.129645,-3.148541 m -6.503459,1.03452 c 0,0 -0.674687,-0.690562 -1.17475,-1.005416 -0.248708,0.468312 -0.425979,0.976312 -0.513291,1.500187 -0.156105,0.92075 0,2.227792 0.36777,3.201459 0.05821,0.150812 0.275167,0.127 0.29898,-0.03175 0.3175,-2.092855 1.021291,-3.66448 1.021291,-3.66448"
|
|
||||||
style="fill:#e2a610;stroke-width:0.264583"
|
|
||||||
id="path2-6" />
|
|
||||||
<path
|
|
||||||
d="m 50.906014,97.636935 c 0,0 -8.252354,0.891646 -11.975042,8.056565 -3.722687,7.16492 -0.558271,11.50937 2.791354,13.09158 3.349626,1.58221 11.789834,2.14048 17.279938,0.83873 5.490104,-1.30175 6.863292,-4.0005 6.606646,-6.60664 -0.373062,-3.80471 -3.907896,-6.14363 -3.907896,-6.14363 0,0 0.140229,-4.699 -3.505729,-7.749647 -3.235854,-2.709333 -7.289271,-1.486958 -7.289271,-1.486958"
|
|
||||||
style="fill:#ffca28;stroke-width:0.264583"
|
|
||||||
id="path3-0" />
|
|
||||||
<path
|
|
||||||
d="m 56.120952,95.996518 c 2.233083,0.727604 2.727854,2.746375 2.566458,3.296709 -0.193146,0.645583 -2.667,-1.867959 -6.344708,-1.717146 -1.285875,0.05292 -0.912813,-0.735542 -0.3175,-1.190625 0.785812,-0.600604 2.106083,-1.034521 4.09575,-0.388938"
|
|
||||||
style="fill:#6d4c41;stroke-width:0.264583"
|
|
||||||
id="path6-6" />
|
|
||||||
<path
|
|
||||||
d="m 56.120952,95.996518 c 2.233083,0.727604 2.727854,2.746375 2.566458,3.296709 -0.193146,0.645583 -2.667,-1.867959 -6.344708,-1.717146 -1.285875,0.05292 -0.912813,-0.735542 -0.3175,-1.190625 0.785812,-0.600604 2.106083,-1.034521 4.09575,-0.388938"
|
|
||||||
style="fill:#6b4b46;stroke-width:0.264583"
|
|
||||||
id="path7-2" />
|
|
||||||
<path
|
|
||||||
d="m 60.042077,103.11381 c 0.280458,-0.19314 1.222375,0.14023 1.486958,1.98438 0.129646,0.90223 0.169333,1.77535 0.169333,1.77535 0,0 -1.11125,-0.99748 -1.47902,-1.69862 -0.463021,-0.88636 -0.642938,-1.74361 -0.177271,-2.06111"
|
|
||||||
style="fill:#e2a610;stroke-width:0.264583"
|
|
||||||
id="path8-6" />
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
|
|
||||||
x="82.355011"
|
|
||||||
y="90.66716"
|
|
||||||
id="text4-9"
|
|
||||||
inkscape:label="$"
|
|
||||||
transform="rotate(20.578693)"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan4-2"
|
|
||||||
style="font-size:19.7556px;fill:#4d4d4d;stroke-width:0.264583"
|
|
||||||
x="82.355011"
|
|
||||||
y="90.66716">$</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
inkscape:label="Logo"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="g7"
|
|
||||||
transform="translate(1.4293676,-48.496402)">
|
|
||||||
<g
|
|
||||||
id="g8"
|
|
||||||
inkscape:label="Favicon"
|
|
||||||
transform="translate(-38.797122,-28.178962)"
|
|
||||||
inkscape:export-filename="../static/logo.svg"
|
|
||||||
inkscape:export-xdpi="96"
|
|
||||||
inkscape:export-ydpi="96">
|
|
||||||
<path
|
|
||||||
d="m 98.874384,115.02659 c 1.775356,-0.39423 4.087816,-2.15635 4.439706,-3.02419 0.20638,-0.508 -0.82285,-1.30175 -1.09802,-1.62189 -0.62971,-0.73025 -0.37571,-1.09009 -0.13229,-1.96057 0.27781,-0.98954 -0.381,-2.08227 -1.31498,-2.51089 -0.933978,-0.42863 -2.05052,-0.29369 -2.989791,0.12435 -0.939271,0.41804 -1.740958,1.09009 -2.526771,1.75154 -0.574145,-0.36248 -1.489604,-1.96321 -2.97127,-0.92339 -1.023938,0.71702 -1.116542,2.27806 -0.98425,3.52425 0.309562,2.87602 1.018645,4.36827 2.354791,4.77043 1.688042,0.508 3.556,0.24078 5.222875,-0.12964"
|
|
||||||
style="fill:#ffca28;stroke-width:0.264583"
|
|
||||||
id="path1-3-3" />
|
|
||||||
<path
|
|
||||||
d="m 101.75834,106.75307 c -0.22489,3.87614 -4.005789,6.44789 -5.799664,7.58031 l 1.164167,1.00013 c 0,0 0.738187,0.0159 1.748895,-0.30692 1.733022,-0.55033 4.265082,-2.10608 4.439712,-3.02419 0.25664,-1.33614 -1.1139,-1.0451 -1.42346,-2.10079 -0.1614,-0.55827 0.78581,-1.61396 -0.12965,-3.14854 m -6.503456,1.03452 c 0,0 -0.674687,-0.69056 -1.17475,-1.00542 -0.248708,0.46832 -0.425979,0.97632 -0.513291,1.50019 -0.156105,0.92075 0,2.22779 0.36777,3.20146 0.05821,0.15081 0.275167,0.127 0.29898,-0.0317 0.3175,-2.09286 1.021291,-3.66448 1.021291,-3.66448"
|
|
||||||
style="fill:#e2a610;stroke-width:0.264583"
|
|
||||||
id="path2-6-6" />
|
|
||||||
<path
|
|
||||||
d="m 90.540009,114.68528 c 0,0 -8.252354,0.89164 -11.975042,8.05656 -3.722687,7.16492 -0.558271,11.50937 2.791354,13.09158 3.349626,1.58221 11.789834,2.14048 17.279938,0.83873 5.490101,-1.30175 6.863291,-4.0005 6.606641,-6.60664 -0.37306,-3.80471 -3.90789,-6.14363 -3.90789,-6.14363 0,0 0.14023,-4.699 -3.50573,-7.74964 -3.235854,-2.70934 -7.289271,-1.48696 -7.289271,-1.48696"
|
|
||||||
style="fill:#ffca28;stroke-width:0.264583"
|
|
||||||
id="path3-0-1" />
|
|
||||||
<path
|
|
||||||
d="m 95.754947,113.04486 c 2.233083,0.7276 2.727854,2.74638 2.566458,3.29671 -0.193146,0.64558 -2.667,-1.86796 -6.344708,-1.71715 -1.285875,0.0529 -0.912813,-0.73554 -0.3175,-1.19062 0.785812,-0.60061 2.106083,-1.03452 4.09575,-0.38894"
|
|
||||||
style="fill:#6d4c41;stroke-width:0.264583"
|
|
||||||
id="path6-6-2" />
|
|
||||||
<path
|
|
||||||
d="m 95.754947,113.04486 c 2.233083,0.7276 2.727854,2.74638 2.566458,3.29671 -0.193146,0.64558 -2.667,-1.86796 -6.344708,-1.71715 -1.285875,0.0529 -0.912813,-0.73554 -0.3175,-1.19062 0.785812,-0.60061 2.106083,-1.03452 4.09575,-0.38894"
|
|
||||||
style="fill:#6b4b46;stroke-width:0.264583"
|
|
||||||
id="path7-2-9" />
|
|
||||||
<path
|
|
||||||
d="m 99.676072,120.16215 c 0.280458,-0.19314 1.222378,0.14023 1.486958,1.98438 0.12965,0.90223 0.16933,1.77535 0.16933,1.77535 0,0 -1.11125,-0.99748 -1.479017,-1.69862 -0.463021,-0.88636 -0.642938,-1.74361 -0.177271,-2.06111"
|
|
||||||
style="fill:#e2a610;stroke-width:0.264583"
|
|
||||||
id="path8-6-3" />
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
|
|
||||||
x="125.45235"
|
|
||||||
y="92.696564"
|
|
||||||
id="text4-9-1"
|
|
||||||
inkscape:label="$"
|
|
||||||
transform="rotate(20.578693)"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan4-2-9"
|
|
||||||
style="font-size:19.7556px;fill:#4d4d4d;stroke-width:0.264583"
|
|
||||||
x="125.45235"
|
|
||||||
y="92.696564">$</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer2"
|
|
||||||
inkscape:label="Text"
|
|
||||||
transform="translate(-1.4293676,48.496402)">
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:17.6389px;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
|
|
||||||
x="57.635151"
|
|
||||||
y="55.655094"
|
|
||||||
id="text1"
|
|
||||||
inkscape:label="SpendSparrow"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan1"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:17.6389px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:-0.529167px;fill:#4d4d4d;stroke:none;stroke-width:0.264583"
|
|
||||||
x="57.635151"
|
|
||||||
y="55.655094">pendSparrow</tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#1a1a1a;stroke-width:0.264583"
|
|
||||||
x="93.314896"
|
|
||||||
y="91.227318"
|
|
||||||
id="text5"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan5"
|
|
||||||
style="stroke-width:0.264583"
|
|
||||||
x="93.314896"
|
|
||||||
y="91.227318" /></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="scale(0.26458333)"
|
|
||||||
id="text6"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:74.6667px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-2px;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect6);display:inline;fill:#1a1a1a;stroke:none" />
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="scale(0.26458333)"
|
|
||||||
id="text2"
|
|
||||||
style="fill:#4d4d4d;text-orientation:auto;-inkscape-font-specification:'Pirata One, Normal';font-family:'Pirata One';font-size:74.66666667px;letter-spacing:-2px;text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect2)" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB |
6
dev.sh
6
dev.sh
@@ -1,9 +1,3 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
|
||||||
go install github.com/a-h/templ/cmd/templ@latest
|
|
||||||
go install github.com/vektra/mockery/v2@latest
|
|
||||||
|
|
||||||
templ generate --watch --proxy="http://localhost:8080" --cmd="go run ." &
|
templ generate --watch --proxy="http://localhost:8080" --cmd="go run ." &
|
||||||
npm run watch
|
npm run watch
|
||||||
|
|
||||||
|
|||||||
65
go.mod
65
go.mod
@@ -1,54 +1,53 @@
|
|||||||
module spend-sparrow
|
module spend-sparrow
|
||||||
|
|
||||||
go 1.24.0
|
go 1.23.0
|
||||||
|
|
||||||
toolchain go1.25.3
|
toolchain go1.24.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/a-h/templ v0.3.960
|
github.com/a-h/templ v0.3.898
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.0
|
github.com/golang-migrate/migrate/v4 v4.18.3
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/mattn/go-sqlite3 v1.14.32
|
github.com/mattn/go-sqlite3 v1.14.28
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2
|
go.opentelemetry.io/contrib/bridges/otelslog v0.11.0
|
||||||
github.com/uptrace/opentelemetry-go-extra/otelsqlx v0.3.2
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0
|
go.opentelemetry.io/otel v1.36.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2
|
||||||
go.opentelemetry.io/otel v1.38.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0
|
go.opentelemetry.io/otel/log v0.12.2
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
|
go.opentelemetry.io/otel/sdk v1.36.0
|
||||||
go.opentelemetry.io/otel/log v0.14.0
|
go.opentelemetry.io/otel/sdk/log v0.12.2
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0
|
go.opentelemetry.io/otel/sdk/metric v1.36.0
|
||||||
go.opentelemetry.io/otel/sdk/log v0.14.0
|
go.opentelemetry.io/otel/trace v1.36.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0
|
golang.org/x/crypto v0.39.0
|
||||||
go.opentelemetry.io/otel/trace v1.38.0
|
golang.org/x/net v0.41.0
|
||||||
golang.org/x/crypto v0.43.0
|
|
||||||
golang.org/x/net v0.46.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
|
||||||
golang.org/x/sys v0.37.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
golang.org/x/text v0.30.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
golang.org/x/text v0.26.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
|
||||||
google.golang.org/grpc v1.75.0 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
|
||||||
google.golang.org/protobuf v1.36.8 // indirect
|
google.golang.org/grpc v1.72.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
128
go.sum
128
go.sum
@@ -1,30 +1,30 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
|
github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s=
|
||||||
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
|
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
|
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
@@ -41,72 +41,68 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c=
|
|
||||||
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTXKkiKNGmLQS7vRsqRxIQTFZpYpHK3IQ=
|
|
||||||
github.com/uptrace/opentelemetry-go-extra/otelsqlx v0.3.2 h1:zA9ZXfdtowo0EKt+t7uqXNlHxPeygrxuFSIroiBVgPU=
|
|
||||||
github.com/uptrace/opentelemetry-go-extra/otelsqlx v0.3.2/go.mod h1:ySXmuW9JLCm/TjsQksuMY/7MNiWqfHnhH2xeT34uOLU=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s=
|
go.opentelemetry.io/contrib/bridges/otelslog v0.11.0 h1:EMIiYTms4Z4m3bBuKp1VmMNRLZcl6j4YbvOPL1IhlWo=
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4=
|
go.opentelemetry.io/contrib/bridges/otelslog v0.11.0/go.mod h1:DIEZmUR7tzuOOVUTDKvkGWtYWSHFV18Qg8+GMb8wPJw=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
|
||||||
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
|
go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc=
|
||||||
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
|
go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
|
go.opentelemetry.io/otel/sdk/log v0.12.2 h1:yNoETvTByVKi7wHvYS6HMcZrN5hFLD7I++1xIZ/k6W0=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
|
go.opentelemetry.io/otel/sdk/log v0.12.2/go.mod h1:DcpdmUXHJgSqN/dh+XMWa7Vf89u9ap0/AAk/XGLnEzY=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
|
||||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
33
input.css
33
input.css
@@ -3,34 +3,37 @@
|
|||||||
@source './static/**/*.js';
|
@source './static/**/*.js';
|
||||||
@source './template/**/*.templ';
|
@source './template/**/*.templ';
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply font-garamond text-gray-700;
|
||||||
|
}
|
||||||
|
input:focus {
|
||||||
|
@apply outline-none ring-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Pirata One";
|
||||||
|
src: url("/static/font/PirataOne-Regular.woff2") format("woff2");
|
||||||
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "EB Garamond";
|
font-family: "EB Garamond";
|
||||||
src: url("/static/font/EBGaramond-VariableFont_wght.woff2") format("woff2");
|
src: url("/static/font/EBGaramond-VariableFont_wght.woff2") format("woff2");
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
@theme {
|
||||||
font-family: "EB Garamond", serif;
|
--font-pirata: "Pirata One", serif;
|
||||||
@apply text-gray-700;
|
--font-garamond: "EB Garamond", serif;
|
||||||
}
|
|
||||||
|
|
||||||
input:focus {
|
|
||||||
@apply outline-none ring-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
@apply cursor-pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button */
|
/* Button */
|
||||||
.button {
|
.button {
|
||||||
transition: all 150ms linear;
|
transition: all 150ms linear;
|
||||||
@apply cursor-pointer border-2 rounded-lg border-transparent;
|
@apply cursor-pointer border-2 rounded-lg border-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-primary:hover,
|
.button-primary:hover,
|
||||||
.button-normal:hover {
|
.button-normal:hover {
|
||||||
transform: translate(-0.25rem, -0.25rem);
|
transform: translate(-0.25rem, -0.25rem);
|
||||||
box-shadow: 3px 3px 3px var(--color-gray-200);
|
box-shadow: 3px 3px 3px var(--color-gray-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-primary {
|
.button-primary {
|
||||||
@@ -58,5 +61,3 @@ button {
|
|||||||
box-shadow: 0 0 0 2px var(--color-gray-200);
|
box-shadow: 0 0 0 2px var(--color-gray-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -14,24 +13,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Auth interface {
|
type Auth interface {
|
||||||
InsertUser(ctx context.Context, user *types.User) error
|
InsertUser(user *types.User) error
|
||||||
UpdateUser(ctx context.Context, user *types.User) error
|
UpdateUser(user *types.User) error
|
||||||
GetUserByEmail(ctx context.Context, email string) (*types.User, error)
|
GetUserByEmail(email string) (*types.User, error)
|
||||||
GetUser(ctx context.Context, userId uuid.UUID) (*types.User, error)
|
GetUser(userId uuid.UUID) (*types.User, error)
|
||||||
DeleteUser(ctx context.Context, userId uuid.UUID) error
|
DeleteUser(userId uuid.UUID) error
|
||||||
|
|
||||||
InsertToken(ctx context.Context, token *types.Token) error
|
InsertToken(token *types.Token) error
|
||||||
GetToken(ctx context.Context, token string) (*types.Token, error)
|
GetToken(token string) (*types.Token, error)
|
||||||
GetTokensByUserIdAndType(ctx context.Context, userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error)
|
GetTokensByUserIdAndType(userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error)
|
||||||
GetTokensBySessionIdAndType(ctx context.Context, sessionId string, tokenType types.TokenType) ([]*types.Token, error)
|
GetTokensBySessionIdAndType(sessionId string, tokenType types.TokenType) ([]*types.Token, error)
|
||||||
DeleteToken(ctx context.Context, token string) error
|
DeleteToken(token string) error
|
||||||
|
|
||||||
InsertSession(ctx context.Context, session *types.Session) error
|
InsertSession(session *types.Session) error
|
||||||
GetSession(ctx context.Context, sessionId string) (*types.Session, error)
|
GetSession(sessionId string) (*types.Session, error)
|
||||||
GetSessions(ctx context.Context, userId uuid.UUID) ([]*types.Session, error)
|
GetSessions(userId uuid.UUID) ([]*types.Session, error)
|
||||||
DeleteSession(ctx context.Context, sessionId string) error
|
DeleteSession(sessionId string) error
|
||||||
DeleteOldSessions(ctx context.Context) error
|
DeleteOldSessions(userId uuid.UUID) error
|
||||||
DeleteOldTokens(ctx context.Context) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthSqlite struct {
|
type AuthSqlite struct {
|
||||||
@@ -42,8 +40,8 @@ func NewAuthSqlite(db *sqlx.DB) *AuthSqlite {
|
|||||||
return &AuthSqlite{db: db}
|
return &AuthSqlite{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) InsertUser(ctx context.Context, user *types.User) error {
|
func (db AuthSqlite) InsertUser(user *types.User) error {
|
||||||
_, err := db.db.ExecContext(ctx, `
|
_, err := db.db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, email_verified_at, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, email_verified_at, is_admin, password, salt, created_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
user.Id, user.Email, user.EmailVerified, user.EmailVerifiedAt, user.IsAdmin, user.Password, user.Salt, user.CreateAt)
|
user.Id, user.Email, user.EmailVerified, user.EmailVerifiedAt, user.IsAdmin, user.Password, user.Salt, user.CreateAt)
|
||||||
@@ -53,29 +51,29 @@ func (db AuthSqlite) InsertUser(ctx context.Context, user *types.User) error {
|
|||||||
return ErrAlreadyExists
|
return ErrAlreadyExists
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.ErrorContext(ctx, "SQL error InsertUser", "err", err)
|
slog.Error("SQL error InsertUser", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) UpdateUser(ctx context.Context, user *types.User) error {
|
func (db AuthSqlite) UpdateUser(user *types.User) error {
|
||||||
_, err := db.db.ExecContext(ctx, `
|
_, err := db.db.Exec(`
|
||||||
UPDATE user
|
UPDATE user
|
||||||
SET email_verified = ?, email_verified_at = ?, password = ?
|
SET email_verified = ?, email_verified_at = ?, password = ?
|
||||||
WHERE user_id = ?`,
|
WHERE user_id = ?`,
|
||||||
user.EmailVerified, user.EmailVerifiedAt, user.Password, user.Id)
|
user.EmailVerified, user.EmailVerifiedAt, user.Password, user.Id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "SQL error UpdateUser", "err", err)
|
slog.Error("SQL error UpdateUser", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) GetUserByEmail(ctx context.Context, email string) (*types.User, error) {
|
func (db AuthSqlite) GetUserByEmail(email string) (*types.User, error) {
|
||||||
var (
|
var (
|
||||||
userId uuid.UUID
|
userId uuid.UUID
|
||||||
emailVerified bool
|
emailVerified bool
|
||||||
@@ -86,7 +84,7 @@ func (db AuthSqlite) GetUserByEmail(ctx context.Context, email string) (*types.U
|
|||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
err := db.db.QueryRowContext(ctx, `
|
err := db.db.QueryRow(`
|
||||||
SELECT user_id, email_verified, email_verified_at, password, salt, created_at
|
SELECT user_id, email_verified, email_verified_at, password, salt, created_at
|
||||||
FROM user
|
FROM user
|
||||||
WHERE email = ?`, email).Scan(&userId, &emailVerified, &emailVerifiedAt, &password, &salt, &createdAt)
|
WHERE email = ?`, email).Scan(&userId, &emailVerified, &emailVerifiedAt, &password, &salt, &createdAt)
|
||||||
@@ -94,7 +92,7 @@ func (db AuthSqlite) GetUserByEmail(ctx context.Context, email string) (*types.U
|
|||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, ErrNotFound
|
return nil, ErrNotFound
|
||||||
} else {
|
} else {
|
||||||
slog.ErrorContext(ctx, "SQL error GetUser", "err", err)
|
slog.Error("SQL error GetUser", "err", err)
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +100,7 @@ func (db AuthSqlite) GetUserByEmail(ctx context.Context, email string) (*types.U
|
|||||||
return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
|
return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) GetUser(ctx context.Context, userId uuid.UUID) (*types.User, error) {
|
func (db AuthSqlite) GetUser(userId uuid.UUID) (*types.User, error) {
|
||||||
var (
|
var (
|
||||||
email string
|
email string
|
||||||
emailVerified bool
|
emailVerified bool
|
||||||
@@ -113,7 +111,7 @@ func (db AuthSqlite) GetUser(ctx context.Context, userId uuid.UUID) (*types.User
|
|||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
err := db.db.QueryRowContext(ctx, `
|
err := db.db.QueryRow(`
|
||||||
SELECT email, email_verified, email_verified_at, password, salt, created_at
|
SELECT email, email_verified, email_verified_at, password, salt, created_at
|
||||||
FROM user
|
FROM user
|
||||||
WHERE user_id = ?`, userId).Scan(&email, &emailVerified, &emailVerifiedAt, &password, &salt, &createdAt)
|
WHERE user_id = ?`, userId).Scan(&email, &emailVerified, &emailVerifiedAt, &password, &salt, &createdAt)
|
||||||
@@ -121,7 +119,7 @@ func (db AuthSqlite) GetUser(ctx context.Context, userId uuid.UUID) (*types.User
|
|||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, ErrNotFound
|
return nil, ErrNotFound
|
||||||
} else {
|
} else {
|
||||||
slog.ErrorContext(ctx, "SQL error GetUser", "err", err)
|
slog.Error("SQL error GetUser", "err", err)
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,78 +127,78 @@ func (db AuthSqlite) GetUser(ctx context.Context, userId uuid.UUID) (*types.User
|
|||||||
return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
|
return types.NewUser(userId, email, emailVerified, emailVerifiedAt, isAdmin, password, salt, createdAt), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) DeleteUser(ctx context.Context, userId uuid.UUID) error {
|
func (db AuthSqlite) DeleteUser(userId uuid.UUID) error {
|
||||||
tx, err := db.db.BeginTx(ctx, nil)
|
tx, err := db.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not start transaction", "err", err)
|
slog.Error("Could not start transaction", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx, "DELETE FROM account WHERE user_id = ?", userId)
|
_, err = tx.Exec("DELETE FROM account WHERE user_id = ?", userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
slog.ErrorContext(ctx, "Could not delete accounts", "err", err)
|
slog.Error("Could not delete accounts", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx, "DELETE FROM token WHERE user_id = ?", userId)
|
_, err = tx.Exec("DELETE FROM token WHERE user_id = ?", userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
slog.ErrorContext(ctx, "Could not delete user tokens", "err", err)
|
slog.Error("Could not delete user tokens", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx, "DELETE FROM session WHERE user_id = ?", userId)
|
_, err = tx.Exec("DELETE FROM session WHERE user_id = ?", userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
slog.ErrorContext(ctx, "Could not delete sessions", "err", err)
|
slog.Error("Could not delete sessions", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx, "DELETE FROM user WHERE user_id = ?", userId)
|
_, err = tx.Exec("DELETE FROM user WHERE user_id = ?", userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
slog.ErrorContext(ctx, "Could not delete user", "err", err)
|
slog.Error("Could not delete user", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx, "DELETE FROM treasure_chest WHERE user_id = ?", userId)
|
_, err = tx.Exec("DELETE FROM treasure_chest WHERE user_id = ?", userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
slog.ErrorContext(ctx, "Could not delete user", "err", err)
|
slog.Error("Could not delete user", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx, "DELETE FROM \"transaction\" WHERE user_id = ?", userId)
|
_, err = tx.Exec("DELETE FROM \"transaction\" WHERE user_id = ?", userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
slog.ErrorContext(ctx, "Could not delete user", "err", err)
|
slog.Error("Could not delete user", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not commit transaction", "err", err)
|
slog.Error("Could not commit transaction", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) InsertToken(ctx context.Context, token *types.Token) error {
|
func (db AuthSqlite) InsertToken(token *types.Token) error {
|
||||||
_, err := db.db.ExecContext(ctx, `
|
_, err := db.db.Exec(`
|
||||||
INSERT INTO token (user_id, session_id, type, token, created_at, expires_at)
|
INSERT INTO token (user_id, session_id, type, token, created_at, expires_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`, token.UserId, token.SessionId, token.Type, token.Token, token.CreatedAt, token.ExpiresAt)
|
VALUES (?, ?, ?, ?, ?, ?)`, token.UserId, token.SessionId, token.Type, token.Token, token.CreatedAt, token.ExpiresAt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not insert token", "err", err)
|
slog.Error("Could not insert token", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) GetToken(ctx context.Context, token string) (*types.Token, error) {
|
func (db AuthSqlite) GetToken(token string) (*types.Token, error) {
|
||||||
var (
|
var (
|
||||||
userId uuid.UUID
|
userId uuid.UUID
|
||||||
sessionId string
|
sessionId string
|
||||||
@@ -211,67 +209,67 @@ func (db AuthSqlite) GetToken(ctx context.Context, token string) (*types.Token,
|
|||||||
expiresAt time.Time
|
expiresAt time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
err := db.db.QueryRowContext(ctx, `
|
err := db.db.QueryRow(`
|
||||||
SELECT user_id, session_id, type, created_at, expires_at
|
SELECT user_id, session_id, type, created_at, expires_at
|
||||||
FROM token
|
FROM token
|
||||||
WHERE token = ?`, token).Scan(&userId, &sessionId, &tokenType, &createdAtStr, &expiresAtStr)
|
WHERE token = ?`, token).Scan(&userId, &sessionId, &tokenType, &createdAtStr, &expiresAtStr)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
slog.InfoContext(ctx, "Token not found", "token", token)
|
slog.Info("Token not found", "token", token)
|
||||||
return nil, ErrNotFound
|
return nil, ErrNotFound
|
||||||
} else {
|
} else {
|
||||||
slog.ErrorContext(ctx, "Could not get token", "err", err)
|
slog.Error("Could not get token", "err", err)
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createdAt, err = time.Parse(time.RFC3339, createdAtStr)
|
createdAt, err = time.Parse(time.RFC3339, createdAtStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not parse token.created_at", "err", err)
|
slog.Error("Could not parse token.created_at", "err", err)
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
expiresAt, err = time.Parse(time.RFC3339, expiresAtStr)
|
expiresAt, err = time.Parse(time.RFC3339, expiresAtStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not parse token.expires_at", "err", err)
|
slog.Error("Could not parse token.expires_at", "err", err)
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt), nil
|
return types.NewToken(userId, sessionId, token, tokenType, createdAt, expiresAt), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) GetTokensByUserIdAndType(ctx context.Context, userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error) {
|
func (db AuthSqlite) GetTokensByUserIdAndType(userId uuid.UUID, tokenType types.TokenType) ([]*types.Token, error) {
|
||||||
query, err := db.db.QueryContext(ctx, `
|
query, err := db.db.Query(`
|
||||||
SELECT token, created_at, expires_at
|
SELECT token, created_at, expires_at
|
||||||
FROM token
|
FROM token
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
AND type = ?`, userId, tokenType)
|
AND type = ?`, userId, tokenType)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not get token", "err", err)
|
slog.Error("Could not get token", "err", err)
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
return getTokensFromQuery(ctx, query, userId, "", tokenType)
|
return getTokensFromQuery(query, userId, "", tokenType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) GetTokensBySessionIdAndType(ctx context.Context, sessionId string, tokenType types.TokenType) ([]*types.Token, error) {
|
func (db AuthSqlite) GetTokensBySessionIdAndType(sessionId string, tokenType types.TokenType) ([]*types.Token, error) {
|
||||||
query, err := db.db.QueryContext(ctx, `
|
query, err := db.db.Query(`
|
||||||
SELECT token, created_at, expires_at
|
SELECT token, created_at, expires_at
|
||||||
FROM token
|
FROM token
|
||||||
WHERE session_id = ?
|
WHERE session_id = ?
|
||||||
AND type = ?`, sessionId, tokenType)
|
AND type = ?`, sessionId, tokenType)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not get token", "err", err)
|
slog.Error("Could not get token", "err", err)
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
return getTokensFromQuery(ctx, query, uuid.Nil, sessionId, tokenType)
|
return getTokensFromQuery(query, uuid.Nil, sessionId, tokenType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTokensFromQuery(ctx context.Context, query *sql.Rows, userId uuid.UUID, sessionId string, tokenType types.TokenType) ([]*types.Token, error) {
|
func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tokenType types.TokenType) ([]*types.Token, error) {
|
||||||
var tokens []*types.Token
|
var tokens []*types.Token
|
||||||
|
|
||||||
hasRows := false
|
hasRows := false
|
||||||
@@ -288,19 +286,19 @@ func getTokensFromQuery(ctx context.Context, query *sql.Rows, userId uuid.UUID,
|
|||||||
|
|
||||||
err := query.Scan(&token, &createdAtStr, &expiresAtStr)
|
err := query.Scan(&token, &createdAtStr, &expiresAtStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not scan token", "err", err)
|
slog.Error("Could not scan token", "err", err)
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
createdAt, err = time.Parse(time.RFC3339, createdAtStr)
|
createdAt, err = time.Parse(time.RFC3339, createdAtStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not parse token.created_at", "err", err)
|
slog.Error("Could not parse token.created_at", "err", err)
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
expiresAt, err = time.Parse(time.RFC3339, expiresAtStr)
|
expiresAt, err = time.Parse(time.RFC3339, expiresAtStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not parse token.expires_at", "err", err)
|
slog.Error("Could not parse token.expires_at", "err", err)
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,92 +312,82 @@ func getTokensFromQuery(ctx context.Context, query *sql.Rows, userId uuid.UUID,
|
|||||||
return tokens, nil
|
return tokens, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) DeleteToken(ctx context.Context, token string) error {
|
func (db AuthSqlite) DeleteToken(token string) error {
|
||||||
_, err := db.db.ExecContext(ctx, "DELETE FROM token WHERE token = ?", token)
|
_, err := db.db.Exec("DELETE FROM token WHERE token = ?", token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not delete token", "err", err)
|
slog.Error("Could not delete token", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) InsertSession(ctx context.Context, session *types.Session) error {
|
func (db AuthSqlite) InsertSession(session *types.Session) error {
|
||||||
_, err := db.db.ExecContext(ctx, `
|
_, err := db.db.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES (?, ?, ?, ?)`, session.Id, session.UserId, session.CreatedAt, session.ExpiresAt)
|
VALUES (?, ?, ?, ?)`, session.Id, session.UserId, session.CreatedAt, session.ExpiresAt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not insert new session", "err", err)
|
slog.Error("Could not insert new session", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) GetSession(ctx context.Context, sessionId string) (*types.Session, error) {
|
func (db AuthSqlite) GetSession(sessionId string) (*types.Session, error) {
|
||||||
var (
|
var (
|
||||||
userId uuid.UUID
|
userId uuid.UUID
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
expiresAt time.Time
|
expiresAt time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
err := db.db.QueryRowContext(ctx, `
|
err := db.db.QueryRow(`
|
||||||
SELECT user_id, created_at, expires_at
|
SELECT user_id, created_at, expires_at
|
||||||
FROM session
|
FROM session
|
||||||
WHERE session_id = ?`, sessionId).Scan(&userId, &createdAt, &expiresAt)
|
WHERE session_id = ?`, sessionId).Scan(&userId, &createdAt, &expiresAt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.WarnContext(ctx, "Session not found", "session-id", sessionId, "err", err)
|
slog.Warn("Session not found", "session-id", sessionId, "err", err)
|
||||||
return nil, ErrNotFound
|
return nil, ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewSession(sessionId, userId, createdAt, expiresAt), nil
|
return types.NewSession(sessionId, userId, createdAt, expiresAt), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) GetSessions(ctx context.Context, userId uuid.UUID) ([]*types.Session, error) {
|
func (db AuthSqlite) GetSessions(userId uuid.UUID) ([]*types.Session, error) {
|
||||||
var sessions []*types.Session
|
var sessions []*types.Session
|
||||||
err := db.db.SelectContext(ctx, &sessions, `
|
err := db.db.Select(&sessions, `
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM session
|
FROM session
|
||||||
WHERE user_id = ?`, userId)
|
WHERE user_id = ?`, userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not get sessions", "err", err)
|
slog.Error("Could not get sessions", "err", err)
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessions, nil
|
return sessions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) DeleteSession(ctx context.Context, sessionId string) error {
|
func (db AuthSqlite) DeleteOldSessions(userId uuid.UUID) error {
|
||||||
|
_, err := db.db.Exec(`
|
||||||
|
DELETE FROM session
|
||||||
|
WHERE expires_at < datetime('now')
|
||||||
|
AND user_id = ?`, userId)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Could not delete old sessions", "err", err)
|
||||||
|
return types.ErrInternal
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db AuthSqlite) DeleteSession(sessionId string) error {
|
||||||
if sessionId != "" {
|
if sessionId != "" {
|
||||||
_, err := db.db.ExecContext(ctx, "DELETE FROM session WHERE session_id = ?", sessionId)
|
_, err := db.db.Exec("DELETE FROM session WHERE session_id = ?", sessionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not delete session", "err", err)
|
slog.Error("Could not delete session", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db AuthSqlite) DeleteOldSessions(ctx context.Context) error {
|
|
||||||
_, err := db.db.ExecContext(ctx, `
|
|
||||||
DELETE FROM session
|
|
||||||
WHERE expires_at < datetime('now')`)
|
|
||||||
if err != nil {
|
|
||||||
slog.ErrorContext(ctx, "Could not delete old sessions", "err", err)
|
|
||||||
return types.ErrInternal
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db AuthSqlite) DeleteOldTokens(ctx context.Context) error {
|
|
||||||
_, err := db.db.ExecContext(ctx, `
|
|
||||||
DELETE FROM token
|
|
||||||
WHERE expires_at < datetime('now')`)
|
|
||||||
if err != nil {
|
|
||||||
slog.ErrorContext(ctx, "Could not delete old tokens", "err", err)
|
|
||||||
return types.ErrInternal
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -13,24 +12,24 @@ var (
|
|||||||
ErrAlreadyExists = errors.New("row already exists")
|
ErrAlreadyExists = errors.New("row already exists")
|
||||||
)
|
)
|
||||||
|
|
||||||
func TransformAndLogDbError(ctx context.Context, module string, r sql.Result, err error) error {
|
func TransformAndLogDbError(module string, r sql.Result, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return ErrNotFound
|
return ErrNotFound
|
||||||
}
|
}
|
||||||
slog.ErrorContext(ctx, "database sql", "module", module, "err", err)
|
slog.Error("database sql", "module", module, "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
if r != nil {
|
if r != nil {
|
||||||
rows, err := r.RowsAffected()
|
rows, err := r.RowsAffected()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "database rows affected", "module", module, "err", err)
|
slog.Error("database rows affected", "module", module, "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
if rows == 0 {
|
if rows == 0 {
|
||||||
slog.InfoContext(ctx, "row not found", "module", module)
|
slog.Info("row not found", "module", module)
|
||||||
return ErrNotFound
|
return ErrNotFound
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"spend-sparrow/internal/types"
|
"spend-sparrow/internal/types"
|
||||||
@@ -21,10 +20,10 @@ func (l migrationLogger) Verbose() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunMigrations(ctx context.Context, db *sqlx.DB, pathPrefix string) error {
|
func RunMigrations(db *sqlx.DB, pathPrefix string) error {
|
||||||
driver, err := sqlite3.WithInstance(db.DB, &sqlite3.Config{})
|
driver, err := sqlite3.WithInstance(db.DB, &sqlite3.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not create Migration instance", "err", err)
|
slog.Error("Could not create Migration instance", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,14 +32,14 @@ func RunMigrations(ctx context.Context, db *sqlx.DB, pathPrefix string) error {
|
|||||||
"",
|
"",
|
||||||
driver)
|
driver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not create migrations instance", "err", err)
|
slog.Error("Could not create migrations instance", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Log = migrationLogger{}
|
m.Log = migrationLogger{}
|
||||||
|
|
||||||
if err = m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
if err = m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||||
slog.ErrorContext(ctx, "Could not run migrations", "err", err)
|
slog.Error("Could not run migrations", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func Run(ctx context.Context, database *sqlx.DB, migrationsPrefix string, env fu
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
err = otelShutdown(ctx)
|
err = otelShutdown(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "error shutting down OpenTelemetry SDK", "err", err)
|
slog.Error("error shutting down OpenTelemetry SDK", "err", err)
|
||||||
}
|
}
|
||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
@@ -47,16 +47,16 @@ func Run(ctx context.Context, database *sqlx.DB, migrationsPrefix string, env fu
|
|||||||
slog.SetDefault(log.NewLogPropagator())
|
slog.SetDefault(log.NewLogPropagator())
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.InfoContext(ctx, "Starting server...")
|
slog.Info("Starting server...")
|
||||||
|
|
||||||
// init server settings
|
// init server settings
|
||||||
serverSettings, err := types.NewSettingsFromEnv(ctx, env)
|
serverSettings, err := types.NewSettingsFromEnv(env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// init db
|
// init db
|
||||||
err = db.RunMigrations(ctx, database, migrationsPrefix)
|
err = db.RunMigrations(database, migrationsPrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not run migrations: %w", err)
|
return fmt.Errorf("could not run migrations: %w", err)
|
||||||
}
|
}
|
||||||
@@ -64,29 +64,28 @@ func Run(ctx context.Context, database *sqlx.DB, migrationsPrefix string, env fu
|
|||||||
// init server
|
// init server
|
||||||
httpServer := &http.Server{
|
httpServer := &http.Server{
|
||||||
Addr: ":" + serverSettings.Port,
|
Addr: ":" + serverSettings.Port,
|
||||||
Handler: createHandlerWithServices(ctx, database, serverSettings),
|
Handler: createHandler(database, serverSettings),
|
||||||
ReadHeaderTimeout: 2 * time.Second,
|
ReadHeaderTimeout: 2 * time.Second,
|
||||||
}
|
}
|
||||||
go startServer(ctx, httpServer)
|
go startServer(httpServer)
|
||||||
|
|
||||||
// graceful shutdown
|
// graceful shutdown
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go shutdownServer(ctx, httpServer, &wg)
|
go shutdownServer(httpServer, ctx, &wg)
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func startServer(ctx context.Context, s *http.Server) {
|
func startServer(s *http.Server) {
|
||||||
slog.InfoContext(ctx, "Starting server", "addr", s.Addr)
|
slog.Info("Starting server", "addr", s.Addr)
|
||||||
if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
slog.ErrorContext(ctx, "error listening and serving", "err", err)
|
slog.Error("error listening and serving", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func shutdownServer(ctx context.Context, s *http.Server, wg *sync.WaitGroup) {
|
func shutdownServer(s *http.Server, ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return
|
return
|
||||||
@@ -97,13 +96,13 @@ func shutdownServer(ctx context.Context, s *http.Server, wg *sync.WaitGroup) {
|
|||||||
shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second)
|
shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := s.Shutdown(shutdownCtx); err != nil {
|
if err := s.Shutdown(shutdownCtx); err != nil {
|
||||||
slog.ErrorContext(ctx, "error shutting down http server", "err", err)
|
slog.Error("error shutting down http server", "err", err)
|
||||||
} else {
|
} else {
|
||||||
slog.InfoContext(ctx, "Gracefully stopped http server", "addr", s.Addr)
|
slog.Info("Gracefully stopped http server", "addr", s.Addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *types.Settings) http.Handler {
|
func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler {
|
||||||
var router = http.NewServeMux()
|
var router = http.NewServeMux()
|
||||||
|
|
||||||
authDb := db.NewAuthSqlite(d)
|
authDb := db.NewAuthSqlite(d)
|
||||||
@@ -117,21 +116,16 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
|||||||
treasureChestService := service.NewTreasureChest(d, randomService, clockService)
|
treasureChestService := service.NewTreasureChest(d, randomService, clockService)
|
||||||
transactionService := service.NewTransaction(d, randomService, clockService)
|
transactionService := service.NewTransaction(d, randomService, clockService)
|
||||||
transactionRecurringService := service.NewTransactionRecurring(d, randomService, clockService, transactionService)
|
transactionRecurringService := service.NewTransactionRecurring(d, randomService, clockService, transactionService)
|
||||||
dashboardService := service.NewDashboard(d)
|
|
||||||
|
|
||||||
render := handler.NewRender()
|
render := handler.NewRender()
|
||||||
indexHandler := handler.NewIndex(render, clockService)
|
indexHandler := handler.NewIndex(render)
|
||||||
dashboardHandler := handler.NewDashboard(render, dashboardService, treasureChestService)
|
|
||||||
authHandler := handler.NewAuth(authService, render)
|
authHandler := handler.NewAuth(authService, render)
|
||||||
accountHandler := handler.NewAccount(accountService, render)
|
accountHandler := handler.NewAccount(accountService, render)
|
||||||
treasureChestHandler := handler.NewTreasureChest(treasureChestService, transactionRecurringService, render)
|
treasureChestHandler := handler.NewTreasureChest(treasureChestService, transactionRecurringService, render)
|
||||||
transactionHandler := handler.NewTransaction(transactionService, accountService, treasureChestService, render)
|
transactionHandler := handler.NewTransaction(transactionService, accountService, treasureChestService, render)
|
||||||
transactionRecurringHandler := handler.NewTransactionRecurring(transactionRecurringService, render)
|
transactionRecurringHandler := handler.NewTransactionRecurring(transactionRecurringService, render)
|
||||||
|
|
||||||
go dailyTaskTimer(ctx, transactionRecurringService, authService)
|
|
||||||
|
|
||||||
indexHandler.Handle(router)
|
indexHandler.Handle(router)
|
||||||
dashboardHandler.Handle(router)
|
|
||||||
accountHandler.Handle(router)
|
accountHandler.Handle(router)
|
||||||
treasureChestHandler.Handle(router)
|
treasureChestHandler.Handle(router)
|
||||||
authHandler.Handle(router)
|
authHandler.Handle(router)
|
||||||
@@ -143,6 +137,7 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
|||||||
|
|
||||||
wrapper := middleware.Wrapper(
|
wrapper := middleware.Wrapper(
|
||||||
router,
|
router,
|
||||||
|
middleware.GenerateRecurringTransactions(transactionRecurringService),
|
||||||
middleware.SecurityHeaders(serverSettings),
|
middleware.SecurityHeaders(serverSettings),
|
||||||
middleware.CacheControl,
|
middleware.CacheControl,
|
||||||
middleware.CrossSiteRequestForgery(authService),
|
middleware.CrossSiteRequestForgery(authService),
|
||||||
@@ -155,24 +150,3 @@ func createHandlerWithServices(ctx context.Context, d *sqlx.DB, serverSettings *
|
|||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func dailyTaskTimer(ctx context.Context, transactionRecurring service.TransactionRecurring, auth service.Auth) {
|
|
||||||
runDailyTasks(ctx, transactionRecurring, auth)
|
|
||||||
ticker := time.NewTicker(24 * time.Hour)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
runDailyTasks(ctx, transactionRecurring, auth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runDailyTasks(ctx context.Context, transactionRecurring service.TransactionRecurring, auth service.Auth) {
|
|
||||||
slog.InfoContext(ctx, "Running daily tasks")
|
|
||||||
_ = transactionRecurring.GenerateTransactions(ctx)
|
|
||||||
_ = auth.CleanupSessionsAndTokens(ctx)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (h AccountImpl) handleAccountPage() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts, err := h.s.GetAll(r.Context(), user)
|
accounts, err := h.s.GetAll(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -72,7 +72,7 @@ func (h AccountImpl) handleAccountItemComp() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
account, err := h.s.Get(r.Context(), user, id)
|
account, err := h.s.Get(user, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -105,13 +105,13 @@ func (h AccountImpl) handleUpdateAccount() http.HandlerFunc {
|
|||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
name := r.FormValue("name")
|
name := r.FormValue("name")
|
||||||
if id == "new" {
|
if id == "new" {
|
||||||
account, err = h.s.Add(r.Context(), user, name)
|
account, err = h.s.Add(user, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
account, err = h.s.UpdateName(r.Context(), user, id, name)
|
account, err = h.s.UpdateName(user, id, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -135,7 +135,7 @@ func (h AccountImpl) handleDeleteAccount() http.HandlerFunc {
|
|||||||
|
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
|
|
||||||
err := h.s.Delete(r.Context(), user, id)
|
err := h.s.Delete(user, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func (handler AuthImpl) handleSignIn() http.HandlerFunc {
|
|||||||
email := r.FormValue("email")
|
email := r.FormValue("email")
|
||||||
password := r.FormValue("password")
|
password := r.FormValue("password")
|
||||||
|
|
||||||
session, user, err := handler.service.SignIn(r.Context(), session, email, password)
|
session, user, err := handler.service.SignIn(session, email, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -98,9 +98,9 @@ func (handler AuthImpl) handleSignIn() http.HandlerFunc {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrInvalidCredentials) {
|
if errors.Is(err, service.ErrInvalidCredentials) {
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Invalid email or password", http.StatusUnauthorized)
|
utils.TriggerToastWithStatus(w, r, "error", "Invalid email or password", http.StatusUnauthorized)
|
||||||
} else {
|
} else {
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "An error occurred", http.StatusInternalServerError)
|
utils.TriggerToastWithStatus(w, r, "error", "An error occurred", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -163,11 +163,11 @@ func (handler AuthImpl) handleVerifyResendComp() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go handler.service.SendVerificationMail(r.Context(), user.Id, user.Email)
|
go handler.service.SendVerificationMail(user.Id, user.Email)
|
||||||
|
|
||||||
_, err := w.Write([]byte("<p class=\"mt-8\">Verification email sent</p>"))
|
_, err := w.Write([]byte("<p class=\"mt-8\">Verification email sent</p>"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(r.Context(), "Could not write response", "err", err)
|
slog.Error("Could not write response", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,7 +178,7 @@ func (handler AuthImpl) handleSignUpVerifyResponsePage() http.HandlerFunc {
|
|||||||
|
|
||||||
token := r.URL.Query().Get("token")
|
token := r.URL.Query().Get("token")
|
||||||
|
|
||||||
err := handler.service.VerifyUserEmail(r.Context(), token)
|
err := handler.service.VerifyUserEmail(token)
|
||||||
|
|
||||||
isVerified := err == nil
|
isVerified := err == nil
|
||||||
comp := auth.VerifyResponseComp(isVerified)
|
comp := auth.VerifyResponseComp(isVerified)
|
||||||
@@ -202,33 +202,33 @@ func (handler AuthImpl) handleSignUp() http.HandlerFunc {
|
|||||||
var password = r.FormValue("password")
|
var password = r.FormValue("password")
|
||||||
|
|
||||||
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) {
|
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) {
|
||||||
slog.InfoContext(r.Context(), "signing up", "email", email)
|
slog.Info("signing up", "email", email)
|
||||||
user, err := handler.service.SignUp(r.Context(), email, password)
|
user, err := handler.service.SignUp(email, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.InfoContext(r.Context(), "Sending verification email", "to", user.Email)
|
slog.Info("Sending verification email", "to", user.Email)
|
||||||
go handler.service.SendVerificationMail(r.Context(), user.Id, user.Email)
|
go handler.service.SendVerificationMail(user.Id, user.Email)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, types.ErrInternal):
|
case errors.Is(err, types.ErrInternal):
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "An error occurred", http.StatusInternalServerError)
|
utils.TriggerToastWithStatus(w, r, "error", "An error occurred", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
case errors.Is(err, service.ErrInvalidEmail):
|
case errors.Is(err, service.ErrInvalidEmail):
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "The email provided is invalid", http.StatusBadRequest)
|
utils.TriggerToastWithStatus(w, r, "error", "The email provided is invalid", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
case errors.Is(err, service.ErrInvalidPassword):
|
case errors.Is(err, service.ErrInvalidPassword):
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", service.ErrInvalidPassword.Error(), http.StatusBadRequest)
|
utils.TriggerToastWithStatus(w, r, "error", service.ErrInvalidPassword.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// If err is "service.ErrAccountExists", then just continue
|
// If err is "service.ErrAccountExists", then just continue
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "success", "An activation link has been send to your email", http.StatusOK)
|
utils.TriggerToastWithStatus(w, r, "success", "An activation link has been send to your email", http.StatusOK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ func (handler AuthImpl) handleSignOut() http.HandlerFunc {
|
|||||||
session := middleware.GetSession(r)
|
session := middleware.GetSession(r)
|
||||||
|
|
||||||
if session != nil {
|
if session != nil {
|
||||||
err := handler.service.SignOut(r.Context(), session.Id)
|
err := handler.service.SignOut(session.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "An error occurred", http.StatusInternalServerError)
|
http.Error(w, "An error occurred", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -288,12 +288,12 @@ func (handler AuthImpl) handleDeleteAccountComp() http.HandlerFunc {
|
|||||||
|
|
||||||
password := r.FormValue("password")
|
password := r.FormValue("password")
|
||||||
|
|
||||||
err := handler.service.DeleteAccount(r.Context(), user, password)
|
err := handler.service.DeleteAccount(user, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrInvalidCredentials) {
|
if errors.Is(err, service.ErrInvalidCredentials) {
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Password not correct", http.StatusBadRequest)
|
utils.TriggerToastWithStatus(w, r, "error", "Password not correct", http.StatusBadRequest)
|
||||||
} else {
|
} else {
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
|
utils.TriggerToastWithStatus(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -327,20 +327,20 @@ func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc {
|
|||||||
session := middleware.GetSession(r)
|
session := middleware.GetSession(r)
|
||||||
user := middleware.GetUser(r)
|
user := middleware.GetUser(r)
|
||||||
if session == nil || user == nil {
|
if session == nil || user == nil {
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Unathorized", http.StatusUnauthorized)
|
utils.TriggerToastWithStatus(w, r, "error", "Unathorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
currPass := r.FormValue("current-password")
|
currPass := r.FormValue("current-password")
|
||||||
newPass := r.FormValue("new-password")
|
newPass := r.FormValue("new-password")
|
||||||
|
|
||||||
err := handler.service.ChangePassword(r.Context(), user, session.Id, currPass, newPass)
|
err := handler.service.ChangePassword(user, session.Id, currPass, newPass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", err.Error(), http.StatusBadRequest)
|
utils.TriggerToastWithStatus(w, r, "error", err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "success", "Password changed", http.StatusOK)
|
utils.TriggerToastWithStatus(w, r, "success", "Password changed", http.StatusOK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,19 +365,19 @@ func (handler AuthImpl) handleForgotPasswordComp() http.HandlerFunc {
|
|||||||
|
|
||||||
email := r.FormValue("email")
|
email := r.FormValue("email")
|
||||||
if email == "" {
|
if email == "" {
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Please enter an email", http.StatusBadRequest)
|
utils.TriggerToastWithStatus(w, r, "error", "Please enter an email", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) {
|
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) {
|
||||||
err := handler.service.SendForgotPasswordMail(r.Context(), email)
|
err := handler.service.SendForgotPasswordMail(email)
|
||||||
return nil, err
|
return nil, err
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
|
utils.TriggerToastWithStatus(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
|
||||||
} else {
|
} else {
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "info", "If the address exists, an email has been sent.", http.StatusOK)
|
utils.TriggerToastWithStatus(w, r, "info", "If the address exists, an email has been sent.", http.StatusOK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,19 +388,19 @@ func (handler AuthImpl) handleForgotPasswordResponseComp() http.HandlerFunc {
|
|||||||
|
|
||||||
pageUrl, err := url.Parse(r.Header.Get("Hx-Current-Url"))
|
pageUrl, err := url.Parse(r.Header.Get("Hx-Current-Url"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(r.Context(), "Could not get current URL", "err", err)
|
slog.Error("Could not get current URL", "err", err)
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
|
utils.TriggerToastWithStatus(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token := pageUrl.Query().Get("token")
|
token := pageUrl.Query().Get("token")
|
||||||
newPass := r.FormValue("new-password")
|
newPass := r.FormValue("new-password")
|
||||||
|
|
||||||
err = handler.service.ForgotPassword(r.Context(), token, newPass)
|
err = handler.service.ForgotPassword(token, newPass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", err.Error(), http.StatusBadRequest)
|
utils.TriggerToastWithStatus(w, r, "error", err.Error(), http.StatusBadRequest)
|
||||||
} else {
|
} else {
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "success", "Password changed", http.StatusOK)
|
utils.TriggerToastWithStatus(w, r, "success", "Password changed", http.StatusOK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,254 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"spend-sparrow/internal/handler/middleware"
|
|
||||||
"spend-sparrow/internal/service"
|
|
||||||
"spend-sparrow/internal/template/dashboard"
|
|
||||||
"spend-sparrow/internal/utils"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Dashboard interface {
|
|
||||||
Handle(router *http.ServeMux)
|
|
||||||
}
|
|
||||||
|
|
||||||
type DashboardImpl struct {
|
|
||||||
r *Render
|
|
||||||
d *service.Dashboard
|
|
||||||
treasureChest service.TreasureChest
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDashboard(r *Render, d *service.Dashboard, treasureChest service.TreasureChest) Dashboard {
|
|
||||||
return DashboardImpl{
|
|
||||||
r: r,
|
|
||||||
d: d,
|
|
||||||
treasureChest: treasureChest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler DashboardImpl) Handle(router *http.ServeMux) {
|
|
||||||
router.Handle("GET /dashboard", handler.handleDashboard())
|
|
||||||
router.Handle("GET /dashboard/main-chart", handler.handleDashboardMainChart())
|
|
||||||
router.Handle("GET /dashboard/treasure-chests", handler.handleDashboardTreasureChests())
|
|
||||||
router.Handle("GET /dashboard/treasure-chest", handler.handleDashboardTreasureChest())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler DashboardImpl) handleDashboard() http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
updateSpan(r)
|
|
||||||
|
|
||||||
user := middleware.GetUser(r)
|
|
||||||
if user == nil {
|
|
||||||
utils.DoRedirect(w, r, "/auth/signin")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
treasureChests, err := handler.treasureChest.GetAll(r.Context(), user)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
comp := dashboard.Dashboard(treasureChests)
|
|
||||||
handler.r.RenderLayoutWithStatus(r, w, comp, user, http.StatusOK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler DashboardImpl) handleDashboardMainChart() http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
updateSpan(r)
|
|
||||||
|
|
||||||
user := middleware.GetUser(r)
|
|
||||||
|
|
||||||
series, err := handler.d.MainChart(r.Context(), user)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
|
|
||||||
accountBuilder := strings.Builder{}
|
|
||||||
savingsBuilder := strings.Builder{}
|
|
||||||
|
|
||||||
for _, entry := range series {
|
|
||||||
accountBuilder.WriteString(fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Value)/100))
|
|
||||||
savingsBuilder.WriteString(fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Savings)/100))
|
|
||||||
}
|
|
||||||
|
|
||||||
account := accountBuilder.String()
|
|
||||||
savings := savingsBuilder.String()
|
|
||||||
|
|
||||||
account = account[:len(account)-1]
|
|
||||||
savings = savings[:len(savings)-1]
|
|
||||||
|
|
||||||
_, err = fmt.Fprintf(w, `
|
|
||||||
{
|
|
||||||
"aria": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"trigger": "axis",
|
|
||||||
"formatter": "<updated by client>"
|
|
||||||
},
|
|
||||||
"xAxis": {
|
|
||||||
"type": "time"
|
|
||||||
},
|
|
||||||
"yAxis": {
|
|
||||||
"axisLabel": {
|
|
||||||
"formatter": "{value} €"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"series": [
|
|
||||||
{
|
|
||||||
"data": [%s],
|
|
||||||
"type": "line",
|
|
||||||
"name": "Account Value"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data": [%s],
|
|
||||||
"type": "line",
|
|
||||||
"name": "Savings"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
`, account, savings)
|
|
||||||
if err != nil {
|
|
||||||
slog.InfoContext(r.Context(), "could not write response", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler DashboardImpl) handleDashboardTreasureChests() http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
updateSpan(r)
|
|
||||||
|
|
||||||
user := middleware.GetUser(r)
|
|
||||||
|
|
||||||
treeList, err := handler.d.TreasureChests(r.Context(), user)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
|
|
||||||
dataBuilder := strings.Builder{}
|
|
||||||
|
|
||||||
for _, item := range treeList {
|
|
||||||
childrenBuilder := strings.Builder{}
|
|
||||||
|
|
||||||
for _, child := range item.Children {
|
|
||||||
if child.Value < 0 {
|
|
||||||
childrenBuilder.WriteString(fmt.Sprintf(`{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, -child.Value))
|
|
||||||
} else {
|
|
||||||
childrenBuilder.WriteString(fmt.Sprintf(`{"name":"%s\n%.2f €","value":%d},`, child.Name, float64(child.Value)/100, child.Value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
children := childrenBuilder.String()
|
|
||||||
children = children[:len(children)-1]
|
|
||||||
dataBuilder.WriteString(fmt.Sprintf(`{"name":"%s","children":[%s]},`, item.Name, children))
|
|
||||||
}
|
|
||||||
data := dataBuilder.String()
|
|
||||||
data = data[:len(data)-1]
|
|
||||||
|
|
||||||
_, err = fmt.Fprintf(w, `
|
|
||||||
{
|
|
||||||
"aria": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"series": [
|
|
||||||
{
|
|
||||||
"data": [%s],
|
|
||||||
"type": "treemap",
|
|
||||||
"name": "Savings"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
`, data)
|
|
||||||
if err != nil {
|
|
||||||
slog.InfoContext(r.Context(), "could not write response", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler DashboardImpl) handleDashboardTreasureChest() http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
updateSpan(r)
|
|
||||||
|
|
||||||
user := middleware.GetUser(r)
|
|
||||||
|
|
||||||
var treasureChestId *uuid.UUID
|
|
||||||
|
|
||||||
treasureChestStr := r.URL.Query().Get("id")
|
|
||||||
if treasureChestStr != "" {
|
|
||||||
id, err := uuid.Parse(treasureChestStr)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, r, fmt.Errorf("could not parse treasure chest: %w", service.ErrBadRequest))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
treasureChestId = &id
|
|
||||||
}
|
|
||||||
|
|
||||||
series, err := handler.d.TreasureChest(r.Context(), user, treasureChestId)
|
|
||||||
if err != nil {
|
|
||||||
handleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
|
|
||||||
valueBuilder := strings.Builder{}
|
|
||||||
|
|
||||||
for _, entry := range series {
|
|
||||||
valueBuilder.WriteString(fmt.Sprintf(`["%s",%.2f],`, entry.Day.Format(time.RFC3339), float64(entry.Value)/100))
|
|
||||||
}
|
|
||||||
|
|
||||||
value := valueBuilder.String()
|
|
||||||
|
|
||||||
if len(value) > 0 {
|
|
||||||
value = value[:len(value)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = fmt.Fprintf(w, `
|
|
||||||
{
|
|
||||||
"aria": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"trigger": "axis",
|
|
||||||
"formatter": "<updated by client>"
|
|
||||||
},
|
|
||||||
"xAxis": {
|
|
||||||
"type": "time"
|
|
||||||
},
|
|
||||||
"yAxis": {
|
|
||||||
"axisLabel": {
|
|
||||||
"formatter": "{value} €"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"series": [
|
|
||||||
{
|
|
||||||
"data": [%s],
|
|
||||||
"type": "line",
|
|
||||||
"name": "Treasure Chest Value"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
`, value)
|
|
||||||
if err != nil {
|
|
||||||
slog.InfoContext(r.Context(), "could not write response", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,17 +15,17 @@ import (
|
|||||||
func handleError(w http.ResponseWriter, r *http.Request, err error) {
|
func handleError(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, service.ErrUnauthorized):
|
case errors.Is(err, service.ErrUnauthorized):
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "You are not autorized to perform this operation.", http.StatusUnauthorized)
|
utils.TriggerToastWithStatus(w, r, "error", "You are not autorized to perform this operation.", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
case errors.Is(err, service.ErrBadRequest):
|
case errors.Is(err, service.ErrBadRequest):
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", extractErrorMessage(err), http.StatusBadRequest)
|
utils.TriggerToastWithStatus(w, r, "error", extractErrorMessage(err), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
case errors.Is(err, db.ErrNotFound):
|
case errors.Is(err, db.ErrNotFound):
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", extractErrorMessage(err), http.StatusNotFound)
|
utils.TriggerToastWithStatus(w, r, "error", extractErrorMessage(err), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Internal Server Error", http.StatusInternalServerError)
|
utils.TriggerToastWithStatus(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractErrorMessage(err error) string {
|
func extractErrorMessage(err error) string {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"spend-sparrow/internal/service"
|
"spend-sparrow/internal/service"
|
||||||
"spend-sparrow/internal/types"
|
"spend-sparrow/internal/types"
|
||||||
@@ -17,21 +16,14 @@ var UserKey ContextKey = "user"
|
|||||||
func Authenticate(service service.Auth) func(http.Handler) http.Handler {
|
func Authenticate(service service.Auth) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
if strings.Contains(r.URL.Path, "/static/") {
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionId := getSessionID(r)
|
sessionId := getSessionID(r)
|
||||||
session, user, _ := service.SignInSession(r.Context(), sessionId)
|
session, user, _ := service.SignInSession(sessionId)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
// Always sign in anonymous
|
// Always sign in anonymous
|
||||||
// This way, we can always generate csrf tokens
|
// This way, we can always generate csrf tokens
|
||||||
if session == nil {
|
if session == nil {
|
||||||
session, err = service.SignInAnonymous(r.Context())
|
session, err = service.SignInAnonymous()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -41,6 +33,7 @@ func Authenticate(service service.Auth) func(http.Handler) http.Handler {
|
|||||||
http.SetCookie(w, &cookie)
|
http.SetCookie(w, &cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
ctx = context.WithValue(ctx, UserKey, user)
|
ctx = context.WithValue(ctx, UserKey, user)
|
||||||
ctx = context.WithValue(ctx, SessionKey, session)
|
ctx = context.WithValue(ctx, SessionKey, session)
|
||||||
|
|
||||||
|
|||||||
@@ -4,27 +4,30 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"spend-sparrow/internal/service"
|
"spend-sparrow/internal/service"
|
||||||
|
"spend-sparrow/internal/types"
|
||||||
"spend-sparrow/internal/utils"
|
"spend-sparrow/internal/utils"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type csrfResponseWriter struct {
|
type csrfResponseWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
|
auth service.Auth
|
||||||
csrfToken string
|
session *types.Session
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCsrfResponseWriter(w http.ResponseWriter, csrfToken string) *csrfResponseWriter {
|
func newCsrfResponseWriter(w http.ResponseWriter, auth service.Auth, session *types.Session) *csrfResponseWriter {
|
||||||
return &csrfResponseWriter{
|
return &csrfResponseWriter{
|
||||||
ResponseWriter: w,
|
ResponseWriter: w,
|
||||||
csrfToken: csrfToken,
|
auth: auth,
|
||||||
|
session: session,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rr *csrfResponseWriter) Write(data []byte) (int, error) {
|
func (rr *csrfResponseWriter) Write(data []byte) (int, error) {
|
||||||
dataStr := string(data)
|
dataStr := string(data)
|
||||||
if rr.csrfToken != "" {
|
csrfToken, err := rr.auth.GetCsrfToken(rr.session)
|
||||||
dataStr = strings.ReplaceAll(dataStr, "CSRF_TOKEN", rr.csrfToken)
|
if err == nil {
|
||||||
|
dataStr = strings.ReplaceAll(dataStr, "CSRF_TOKEN", csrfToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
return rr.ResponseWriter.Write([]byte(dataStr))
|
return rr.ResponseWriter.Write([]byte(dataStr))
|
||||||
@@ -33,13 +36,6 @@ func (rr *csrfResponseWriter) Write(data []byte) (int, error) {
|
|||||||
func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler {
|
func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
if strings.Contains(r.URL.Path, "/static/") {
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
session := GetSession(r)
|
session := GetSession(r)
|
||||||
|
|
||||||
if r.Method == http.MethodPost ||
|
if r.Method == http.MethodPost ||
|
||||||
@@ -48,10 +44,10 @@ func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler
|
|||||||
r.Method == http.MethodPatch {
|
r.Method == http.MethodPatch {
|
||||||
csrfToken := r.Header.Get("Csrf-Token")
|
csrfToken := r.Header.Get("Csrf-Token")
|
||||||
|
|
||||||
if session == nil || csrfToken == "" || !auth.IsCsrfTokenValid(ctx, csrfToken, session.Id) {
|
if session == nil || csrfToken == "" || !auth.IsCsrfTokenValid(csrfToken, session.Id) {
|
||||||
slog.InfoContext(ctx, "CSRF-Token not correct", "token", csrfToken)
|
slog.Info("CSRF-Token not correct", "token", csrfToken)
|
||||||
if r.Header.Get("Hx-Request") == "true" {
|
if r.Header.Get("Hx-Request") == "true" {
|
||||||
utils.TriggerToastWithStatus(ctx, w, r, "error", "CSRF-Token not correct", http.StatusBadRequest)
|
utils.TriggerToastWithStatus(w, r, "error", "CSRF-Token not correct", http.StatusBadRequest)
|
||||||
} else {
|
} else {
|
||||||
http.Error(w, "CSRF-Token not correct", http.StatusBadRequest)
|
http.Error(w, "CSRF-Token not correct", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
@@ -59,17 +55,7 @@ func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := auth.GetCsrfToken(ctx, session)
|
responseWriter := newCsrfResponseWriter(w, auth, session)
|
||||||
if err != nil {
|
|
||||||
if r.Header.Get("Hx-Request") == "true" {
|
|
||||||
utils.TriggerToastWithStatus(ctx, w, r, "error", "Could not generate CSRF Token", http.StatusBadRequest)
|
|
||||||
} else {
|
|
||||||
http.Error(w, "Could not generate CSRF Token", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
responseWriter := newCsrfResponseWriter(w, token)
|
|
||||||
next.ServeHTTP(responseWriter, r)
|
next.ServeHTTP(responseWriter, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"spend-sparrow/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateRecurringTransactions(transactionRecurring service.TransactionRecurring) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user := GetUser(r)
|
||||||
|
if user == nil || r.Method != http.MethodGet {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = transactionRecurring.GenerateTransactions(user)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ func Gzip(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
err := gz.Close()
|
err := gz.Close()
|
||||||
if err != nil && !errors.Is(err, http.ErrBodyNotAllowed) {
|
if err != nil && !errors.Is(err, http.ErrBodyNotAllowed) {
|
||||||
slog.ErrorContext(r.Context(), "Gzip: could not close Writer", "err", err)
|
slog.Error("Gzip: could not close Writer", "err", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
type WrappedWriter struct {
|
type WrappedWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
|
|
||||||
StatusCode int
|
StatusCode int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ func Log(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
next.ServeHTTP(wrapped, r)
|
next.ServeHTTP(wrapped, r)
|
||||||
|
|
||||||
slog.InfoContext(r.Context(), "request",
|
slog.Info("request",
|
||||||
"remoteAddr", r.RemoteAddr,
|
"remoteAddr", r.RemoteAddr,
|
||||||
"status", wrapped.StatusCode,
|
"status", wrapped.StatusCode,
|
||||||
"method", r.Method,
|
"method", r.Method,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package middleware
|
|||||||
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
// Wrapper wraps a list of handlers together.
|
// Chain list of handlers together.
|
||||||
func Wrapper(next http.Handler, handlers ...func(http.Handler) http.Handler) http.Handler {
|
func Wrapper(next http.Handler, handlers ...func(http.Handler) http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
lastHandler := next
|
lastHandler := next
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func (render *Render) RenderWithStatus(r *http.Request, w http.ResponseWriter, c
|
|||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
err := comp.Render(r.Context(), w)
|
err := comp.Render(r.Context(), w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(r.Context(), "Failed to render layout", "err", err)
|
slog.Error("Failed to render layout", "err", err)
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"spend-sparrow/internal/handler/middleware"
|
"spend-sparrow/internal/handler/middleware"
|
||||||
"spend-sparrow/internal/service"
|
|
||||||
"spend-sparrow/internal/template"
|
"spend-sparrow/internal/template"
|
||||||
"spend-sparrow/internal/utils"
|
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
)
|
)
|
||||||
@@ -15,14 +13,12 @@ type Index interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IndexImpl struct {
|
type IndexImpl struct {
|
||||||
r *Render
|
render *Render
|
||||||
c service.Clock
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIndex(r *Render, c service.Clock) Index {
|
func NewIndex(render *Render) Index {
|
||||||
return IndexImpl{
|
return IndexImpl{
|
||||||
r: r,
|
render: render,
|
||||||
c: c,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,8 +33,6 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
|
|||||||
|
|
||||||
user := middleware.GetUser(r)
|
user := middleware.GetUser(r)
|
||||||
|
|
||||||
htmx := utils.IsHtmx(r)
|
|
||||||
|
|
||||||
var comp templ.Component
|
var comp templ.Component
|
||||||
|
|
||||||
var status int
|
var status int
|
||||||
@@ -47,19 +41,14 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
|
|||||||
status = http.StatusNotFound
|
status = http.StatusNotFound
|
||||||
} else {
|
} else {
|
||||||
if user != nil {
|
if user != nil {
|
||||||
utils.DoRedirect(w, r, "/dashboard")
|
comp = template.Dashboard()
|
||||||
return
|
|
||||||
} else {
|
} else {
|
||||||
comp = template.Index()
|
comp = template.Index()
|
||||||
}
|
}
|
||||||
status = http.StatusOK
|
status = http.StatusOK
|
||||||
}
|
}
|
||||||
|
|
||||||
if htmx {
|
handler.render.RenderLayoutWithStatus(r, w, comp, user, status)
|
||||||
handler.r.RenderWithStatus(r, w, comp, status)
|
|
||||||
} else {
|
|
||||||
handler.r.RenderLayoutWithStatus(r, w, comp, user, status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import (
|
|||||||
|
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Transaction interface {
|
type Transaction interface {
|
||||||
@@ -54,26 +56,28 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentSpan := trace.SpanFromContext(r.Context())
|
||||||
|
currentSpan.SetAttributes(attribute.String("", "test"))
|
||||||
|
|
||||||
filter := types.TransactionItemsFilter{
|
filter := types.TransactionItemsFilter{
|
||||||
AccountId: r.URL.Query().Get("account-id"),
|
AccountId: r.URL.Query().Get("account-id"),
|
||||||
TreasureChestId: r.URL.Query().Get("treasure-chest-id"),
|
TreasureChestId: r.URL.Query().Get("treasure-chest-id"),
|
||||||
Error: r.URL.Query().Get("error"),
|
Error: r.URL.Query().Get("error"),
|
||||||
Page: r.URL.Query().Get("page"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transactions, err := h.s.GetAll(r.Context(), user, filter)
|
transactions, err := h.s.GetAll(user, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts, err := h.account.GetAll(r.Context(), user)
|
accounts, err := h.account.GetAll(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
treasureChests, err := h.treasureChest.GetAll(r.Context(), user)
|
treasureChests, err := h.treasureChest.GetAll(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -101,13 +105,13 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts, err := h.account.GetAll(r.Context(), user)
|
accounts, err := h.account.GetAll(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
treasureChests, err := h.treasureChest.GetAll(r.Context(), user)
|
treasureChests, err := h.treasureChest.GetAll(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -120,7 +124,7 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction, err := h.s.Get(r.Context(), user, id)
|
transaction, err := h.s.Get(user, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -208,26 +212,26 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
|
|||||||
|
|
||||||
var transaction *types.Transaction
|
var transaction *types.Transaction
|
||||||
if idStr == "new" {
|
if idStr == "new" {
|
||||||
transaction, err = h.s.Add(r.Context(), nil, user, input)
|
transaction, err = h.s.Add(nil, user, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
transaction, err = h.s.Update(r.Context(), user, input)
|
transaction, err = h.s.Update(user, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts, err := h.account.GetAll(r.Context(), user)
|
accounts, err := h.account.GetAll(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
treasureChests, err := h.treasureChest.GetAll(r.Context(), user)
|
treasureChests, err := h.treasureChest.GetAll(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -249,13 +253,13 @@ func (h TransactionImpl) handleRecalculate() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := h.s.RecalculateBalances(r.Context(), user)
|
err := h.s.RecalculateBalances(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "success", "Balances recalculated, please refresh", http.StatusOK)
|
utils.TriggerToastWithStatus(w, r, "success", "Balances recalculated, please refresh", http.StatusOK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +275,7 @@ func (h TransactionImpl) handleDeleteTransaction() http.HandlerFunc {
|
|||||||
|
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
|
|
||||||
err := h.s.Delete(r.Context(), user, id)
|
err := h.s.Delete(user, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -70,13 +70,13 @@ func (h TransactionRecurringImpl) handleUpdateTransactionRecurring() http.Handle
|
|||||||
}
|
}
|
||||||
|
|
||||||
if input.Id == "new" {
|
if input.Id == "new" {
|
||||||
_, err := h.s.Add(r.Context(), user, input)
|
_, err := h.s.Add(user, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_, err := h.s.Update(r.Context(), user, input)
|
_, err := h.s.Update(user, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -101,7 +101,7 @@ func (h TransactionRecurringImpl) handleDeleteTransactionRecurring() http.Handle
|
|||||||
accountId := r.URL.Query().Get("account-id")
|
accountId := r.URL.Query().Get("account-id")
|
||||||
treasureChestId := r.URL.Query().Get("treasure-chest-id")
|
treasureChestId := r.URL.Query().Get("treasure-chest-id")
|
||||||
|
|
||||||
err := h.s.Delete(r.Context(), user, id)
|
err := h.s.Delete(user, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -115,16 +115,16 @@ func (h TransactionRecurringImpl) renderItems(w http.ResponseWriter, r *http.Req
|
|||||||
var transactionsRecurring []*types.TransactionRecurring
|
var transactionsRecurring []*types.TransactionRecurring
|
||||||
var err error
|
var err error
|
||||||
if accountId == "" && treasureChestId == "" {
|
if accountId == "" && treasureChestId == "" {
|
||||||
utils.TriggerToastWithStatus(r.Context(), w, r, "error", "Please select an account or treasure chest", http.StatusBadRequest)
|
utils.TriggerToastWithStatus(w, r, "error", "Please select an account or treasure chest", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
if accountId != "" {
|
if accountId != "" {
|
||||||
transactionsRecurring, err = h.s.GetAllByAccount(r.Context(), user, accountId)
|
transactionsRecurring, err = h.s.GetAllByAccount(user, accountId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
transactionsRecurring, err = h.s.GetAllByTreasureChest(r.Context(), user, treasureChestId)
|
transactionsRecurring, err = h.s.GetAllByTreasureChest(user, treasureChestId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -48,13 +48,13 @@ func (h TreasureChestImpl) handleTreasureChestPage() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
treasureChests, err := h.s.GetAll(r.Context(), user)
|
treasureChests, err := h.s.GetAll(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionsRecurring, err := h.transactionRecurring.GetAll(r.Context(), user)
|
transactionsRecurring, err := h.transactionRecurring.GetAll(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -77,7 +77,7 @@ func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
treasureChests, err := h.s.GetAll(r.Context(), user)
|
treasureChests, err := h.s.GetAll(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -90,13 +90,13 @@ func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
treasureChest, err := h.s.Get(r.Context(), user, id)
|
treasureChest, err := h.s.Get(user, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(r.Context(), user, treasureChest.Id.String())
|
transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(user, treasureChest.Id.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -132,20 +132,20 @@ func (h TreasureChestImpl) handleUpdateTreasureChest() http.HandlerFunc {
|
|||||||
parentId := r.FormValue("parent-id")
|
parentId := r.FormValue("parent-id")
|
||||||
name := r.FormValue("name")
|
name := r.FormValue("name")
|
||||||
if id == "new" {
|
if id == "new" {
|
||||||
treasureChest, err = h.s.Add(r.Context(), user, parentId, name)
|
treasureChest, err = h.s.Add(user, parentId, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
treasureChest, err = h.s.Update(r.Context(), user, id, parentId, name)
|
treasureChest, err = h.s.Update(user, id, parentId, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(r.Context(), user, treasureChest.Id.String())
|
transactionsRecurring, err := h.transactionRecurring.GetAllByTreasureChest(user, treasureChest.Id.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -171,7 +171,7 @@ func (h TreasureChestImpl) handleDeleteTreasureChest() http.HandlerFunc {
|
|||||||
|
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
|
|
||||||
err := h.s.Delete(r.Context(), user, id)
|
err := h.s.Delete(user, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, r, err)
|
handleError(w, r, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewLogPropagator() *slog.Logger {
|
func NewLogPropagator() *slog.Logger {
|
||||||
return slog.New(&logHandler{
|
|
||||||
console: slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}),
|
console := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})
|
||||||
otel: otelslog.NewHandler("spend-sparrow"),
|
otel := otelslog.NewHandler("spend-sparrow")
|
||||||
})
|
return slog.New(&logHandler{console, otel})
|
||||||
}
|
}
|
||||||
|
|
||||||
type logHandler struct {
|
type logHandler struct {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package internal
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
@@ -14,13 +13,11 @@ import (
|
|||||||
"go.opentelemetry.io/otel/propagation"
|
"go.opentelemetry.io/otel/propagation"
|
||||||
"go.opentelemetry.io/otel/sdk/log"
|
"go.opentelemetry.io/otel/sdk/log"
|
||||||
"go.opentelemetry.io/otel/sdk/metric"
|
"go.opentelemetry.io/otel/sdk/metric"
|
||||||
"go.opentelemetry.io/otel/sdk/resource"
|
|
||||||
"go.opentelemetry.io/otel/sdk/trace"
|
"go.opentelemetry.io/otel/sdk/trace"
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
otelEndpoint = "otel-collector:4317"
|
otelEndpoint = "192.168.188.155:4317"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setupOTelSDK bootstraps the OpenTelemetry pipeline.
|
// setupOTelSDK bootstraps the OpenTelemetry pipeline.
|
||||||
@@ -50,16 +47,8 @@ func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
|
|||||||
prop := newPropagator()
|
prop := newPropagator()
|
||||||
otel.SetTextMapPropagator(prop)
|
otel.SetTextMapPropagator(prop)
|
||||||
|
|
||||||
resources, err := resource.New(
|
|
||||||
ctx,
|
|
||||||
resource.WithAttributes(semconv.ServiceName("spend-sparrow")),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
slog.ErrorContext(ctx, "failed to create resource", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up trace provider.
|
// Set up trace provider.
|
||||||
tracerProvider, err := newTracerProvider(ctx, resources)
|
tracerProvider, err := newTracerProvider(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleErr(ctx, err)
|
handleErr(ctx, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -68,7 +57,7 @@ func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
|
|||||||
otel.SetTracerProvider(tracerProvider)
|
otel.SetTracerProvider(tracerProvider)
|
||||||
|
|
||||||
// Set up meter provider.
|
// Set up meter provider.
|
||||||
meterProvider, err := newMeterProvider(ctx, resources)
|
meterProvider, err := newMeterProvider(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleErr(ctx, err)
|
handleErr(ctx, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -77,7 +66,7 @@ func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
|
|||||||
otel.SetMeterProvider(meterProvider)
|
otel.SetMeterProvider(meterProvider)
|
||||||
|
|
||||||
// Set up logger provider.
|
// Set up logger provider.
|
||||||
loggerProvider, err := newLoggerProvider(ctx, resources)
|
loggerProvider, err := newLoggerProvider(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleErr(ctx, err)
|
handleErr(ctx, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -95,9 +84,8 @@ func newPropagator() propagation.TextMapPropagator {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTracerProvider(ctx context.Context, resource *resource.Resource) (*trace.TracerProvider, error) {
|
func newTracerProvider(ctx context.Context) (*trace.TracerProvider, error) {
|
||||||
exp, err := otlptracegrpc.New(
|
exp, err := otlptracegrpc.New(ctx,
|
||||||
ctx,
|
|
||||||
otlptracegrpc.WithEndpoint(otelEndpoint),
|
otlptracegrpc.WithEndpoint(otelEndpoint),
|
||||||
otlptracegrpc.WithInsecure(),
|
otlptracegrpc.WithInsecure(),
|
||||||
)
|
)
|
||||||
@@ -105,15 +93,11 @@ func newTracerProvider(ctx context.Context, resource *resource.Resource) (*trace
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return trace.NewTracerProvider(
|
return trace.NewTracerProvider(trace.WithBatcher(exp)), nil
|
||||||
trace.WithBatcher(exp),
|
|
||||||
trace.WithResource(resource),
|
|
||||||
), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMeterProvider(ctx context.Context, resource *resource.Resource) (*metric.MeterProvider, error) {
|
func newMeterProvider(ctx context.Context) (*metric.MeterProvider, error) {
|
||||||
exp, err := otlpmetricgrpc.New(
|
exp, err := otlpmetricgrpc.New(ctx,
|
||||||
ctx,
|
|
||||||
otlpmetricgrpc.WithInsecure(),
|
otlpmetricgrpc.WithInsecure(),
|
||||||
otlpmetricgrpc.WithEndpoint(otelEndpoint))
|
otlpmetricgrpc.WithEndpoint(otelEndpoint))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -121,12 +105,12 @@ func newMeterProvider(ctx context.Context, resource *resource.Resource) (*metric
|
|||||||
}
|
}
|
||||||
|
|
||||||
return metric.NewMeterProvider(
|
return metric.NewMeterProvider(
|
||||||
metric.WithReader(metric.NewPeriodicReader(exp, metric.WithInterval(15*time.Second))),
|
metric.WithReader(
|
||||||
metric.WithResource(resource),
|
metric.NewPeriodicReader(
|
||||||
), nil
|
exp, metric.WithInterval(15*time.Second)))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLoggerProvider(ctx context.Context, resource *resource.Resource) (*log.LoggerProvider, error) {
|
func newLoggerProvider(ctx context.Context) (*log.LoggerProvider, error) {
|
||||||
logExporter, err := otlploggrpc.New(
|
logExporter, err := otlploggrpc.New(
|
||||||
ctx,
|
ctx,
|
||||||
otlploggrpc.WithInsecure(),
|
otlploggrpc.WithInsecure(),
|
||||||
@@ -137,7 +121,6 @@ func newLoggerProvider(ctx context.Context, resource *resource.Resource) (*log.L
|
|||||||
|
|
||||||
loggerProvider := log.NewLoggerProvider(
|
loggerProvider := log.NewLoggerProvider(
|
||||||
log.WithProcessor(log.NewBatchProcessor(logExporter)),
|
log.WithProcessor(log.NewBatchProcessor(logExporter)),
|
||||||
log.WithResource(resource),
|
|
||||||
)
|
)
|
||||||
return loggerProvider, nil
|
return loggerProvider, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -13,11 +12,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Account interface {
|
type Account interface {
|
||||||
Add(ctx context.Context, user *types.User, name string) (*types.Account, error)
|
Add(user *types.User, name string) (*types.Account, error)
|
||||||
UpdateName(ctx context.Context, user *types.User, id string, name string) (*types.Account, error)
|
UpdateName(user *types.User, id string, name string) (*types.Account, error)
|
||||||
Get(ctx context.Context, user *types.User, id string) (*types.Account, error)
|
Get(user *types.User, id string) (*types.Account, error)
|
||||||
GetAll(ctx context.Context, user *types.User) ([]*types.Account, error)
|
GetAll(user *types.User) ([]*types.Account, error)
|
||||||
Delete(ctx context.Context, user *types.User, id string) error
|
Delete(user *types.User, id string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountImpl struct {
|
type AccountImpl struct {
|
||||||
@@ -34,12 +33,12 @@ func NewAccount(db *sqlx.DB, random Random, clock Clock) Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s AccountImpl) Add(ctx context.Context, user *types.User, name string) (*types.Account, error) {
|
func (s AccountImpl) Add(user *types.User, name string) (*types.Account, error) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
newId, err := s.random.UUID(ctx)
|
newId, err := s.random.UUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
@@ -65,10 +64,10 @@ func (s AccountImpl) Add(ctx context.Context, user *types.User, name string) (*t
|
|||||||
UpdatedBy: nil,
|
UpdatedBy: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := s.db.NamedExecContext(ctx, `
|
r, err := s.db.NamedExec(`
|
||||||
INSERT INTO account (id, user_id, name, current_balance, oink_balance, created_at, created_by)
|
INSERT INTO account (id, user_id, name, current_balance, oink_balance, created_at, created_by)
|
||||||
VALUES (:id, :user_id, :name, :current_balance, :oink_balance, :created_at, :created_by)`, account)
|
VALUES (:id, :user_id, :name, :current_balance, :oink_balance, :created_at, :created_by)`, account)
|
||||||
err = db.TransformAndLogDbError(ctx, "account Insert", r, err)
|
err = db.TransformAndLogDbError("account Insert", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -76,7 +75,7 @@ func (s AccountImpl) Add(ctx context.Context, user *types.User, name string) (*t
|
|||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s AccountImpl) UpdateName(ctx context.Context, user *types.User, id string, name string) (*types.Account, error) {
|
func (s AccountImpl) UpdateName(user *types.User, id string, name string) (*types.Account, error) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
@@ -86,12 +85,12 @@ func (s AccountImpl) UpdateName(ctx context.Context, user *types.User, id string
|
|||||||
}
|
}
|
||||||
uuid, err := uuid.Parse(id)
|
uuid, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "account update", "err", err)
|
slog.Error("account update", "err", err)
|
||||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := s.db.BeginTxx(ctx, nil)
|
tx, err := s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError(ctx, "account Update", nil, err)
|
err = db.TransformAndLogDbError("account Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -100,8 +99,8 @@ func (s AccountImpl) UpdateName(ctx context.Context, user *types.User, id string
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
var account types.Account
|
var account types.Account
|
||||||
err = tx.GetContext(ctx, &account, `SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
err = tx.Get(&account, `SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||||
err = db.TransformAndLogDbError(ctx, "account Update", nil, err)
|
err = db.TransformAndLogDbError("account Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNotFound) {
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
return nil, fmt.Errorf("account %v not found: %w", id, ErrBadRequest)
|
return nil, fmt.Errorf("account %v not found: %w", id, ErrBadRequest)
|
||||||
@@ -114,7 +113,7 @@ func (s AccountImpl) UpdateName(ctx context.Context, user *types.User, id string
|
|||||||
account.UpdatedAt = ×tamp
|
account.UpdatedAt = ×tamp
|
||||||
account.UpdatedBy = &user.Id
|
account.UpdatedBy = &user.Id
|
||||||
|
|
||||||
r, err := tx.NamedExecContext(ctx, `
|
r, err := tx.NamedExec(`
|
||||||
UPDATE account
|
UPDATE account
|
||||||
SET
|
SET
|
||||||
name = :name,
|
name = :name,
|
||||||
@@ -122,13 +121,13 @@ func (s AccountImpl) UpdateName(ctx context.Context, user *types.User, id string
|
|||||||
updated_by = :updated_by
|
updated_by = :updated_by
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
AND user_id = :user_id`, account)
|
AND user_id = :user_id`, account)
|
||||||
err = db.TransformAndLogDbError(ctx, "account Update", r, err)
|
err = db.TransformAndLogDbError("account Update", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
err = db.TransformAndLogDbError(ctx, "account Update", nil, err)
|
err = db.TransformAndLogDbError("account Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -136,37 +135,37 @@ func (s AccountImpl) UpdateName(ctx context.Context, user *types.User, id string
|
|||||||
return &account, nil
|
return &account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s AccountImpl) Get(ctx context.Context, user *types.User, id string) (*types.Account, error) {
|
func (s AccountImpl) Get(user *types.User, id string) (*types.Account, error) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
uuid, err := uuid.Parse(id)
|
uuid, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "account get", "err", err)
|
slog.Error("account get", "err", err)
|
||||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
var account types.Account
|
var account types.Account
|
||||||
err = s.db.GetContext(ctx, &account, `
|
err = s.db.Get(&account, `
|
||||||
SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||||
err = db.TransformAndLogDbError(ctx, "account Get", nil, err)
|
err = db.TransformAndLogDbError("account Get", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "account get", "err", err)
|
slog.Error("account get", "err", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &account, nil
|
return &account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s AccountImpl) GetAll(ctx context.Context, user *types.User) ([]*types.Account, error) {
|
func (s AccountImpl) GetAll(user *types.User) ([]*types.Account, error) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts := make([]*types.Account, 0)
|
accounts := make([]*types.Account, 0)
|
||||||
err := s.db.SelectContext(ctx, &accounts, `
|
err := s.db.Select(&accounts, `
|
||||||
SELECT * FROM account WHERE user_id = ? ORDER BY name`, user.Id)
|
SELECT * FROM account WHERE user_id = ? ORDER BY name`, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "account GetAll", nil, err)
|
err = db.TransformAndLogDbError("account GetAll", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -174,18 +173,18 @@ func (s AccountImpl) GetAll(ctx context.Context, user *types.User) ([]*types.Acc
|
|||||||
return accounts, nil
|
return accounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s AccountImpl) Delete(ctx context.Context, user *types.User, id string) error {
|
func (s AccountImpl) Delete(user *types.User, id string) error {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return ErrUnauthorized
|
return ErrUnauthorized
|
||||||
}
|
}
|
||||||
uuid, err := uuid.Parse(id)
|
uuid, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "account delete", "err", err)
|
slog.Error("account delete", "err", err)
|
||||||
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := s.db.BeginTxx(ctx, nil)
|
tx, err := s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError(ctx, "account Delete", nil, err)
|
err = db.TransformAndLogDbError("account Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -194,8 +193,8 @@ func (s AccountImpl) Delete(ctx context.Context, user *types.User, id string) er
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
transactionsCount := 0
|
transactionsCount := 0
|
||||||
err = tx.GetContext(ctx, &transactionsCount, `SELECT COUNT(*) FROM "transaction" WHERE user_id = ? AND account_id = ?`, user.Id, uuid)
|
err = tx.Get(&transactionsCount, `SELECT COUNT(*) FROM "transaction" WHERE user_id = ? AND account_id = ?`, user.Id, uuid)
|
||||||
err = db.TransformAndLogDbError(ctx, "account Delete", nil, err)
|
err = db.TransformAndLogDbError("account Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -203,14 +202,14 @@ func (s AccountImpl) Delete(ctx context.Context, user *types.User, id string) er
|
|||||||
return fmt.Errorf("account has transactions, cannot delete: %w", ErrBadRequest)
|
return fmt.Errorf("account has transactions, cannot delete: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := tx.ExecContext(ctx, "DELETE FROM account WHERE id = ? and user_id = ?", uuid, user.Id)
|
res, err := tx.Exec("DELETE FROM account WHERE id = ? and user_id = ?", uuid, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "account Delete", res, err)
|
err = db.TransformAndLogDbError("account Delete", res, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
err = db.TransformAndLogDbError(ctx, "account Delete", nil, err)
|
err = db.TransformAndLogDbError("account Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,26 +26,24 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Auth interface {
|
type Auth interface {
|
||||||
SignUp(ctx context.Context, email string, password string) (*types.User, error)
|
SignUp(email string, password string) (*types.User, error)
|
||||||
SendVerificationMail(ctx context.Context, userId uuid.UUID, email string)
|
SendVerificationMail(userId uuid.UUID, email string)
|
||||||
VerifyUserEmail(ctx context.Context, token string) error
|
VerifyUserEmail(token string) error
|
||||||
|
|
||||||
SignIn(ctx context.Context, session *types.Session, email string, password string) (*types.Session, *types.User, error)
|
SignIn(session *types.Session, email string, password string) (*types.Session, *types.User, error)
|
||||||
SignInSession(ctx context.Context, sessionId string) (*types.Session, *types.User, error)
|
SignInSession(sessionId string) (*types.Session, *types.User, error)
|
||||||
SignInAnonymous(ctx context.Context) (*types.Session, error)
|
SignInAnonymous() (*types.Session, error)
|
||||||
SignOut(ctx context.Context, sessionId string) error
|
SignOut(sessionId string) error
|
||||||
|
|
||||||
DeleteAccount(ctx context.Context, user *types.User, currPass string) error
|
DeleteAccount(user *types.User, currPass string) error
|
||||||
|
|
||||||
ChangePassword(ctx context.Context, user *types.User, sessionId string, currPass, newPass string) error
|
ChangePassword(user *types.User, sessionId string, currPass, newPass string) error
|
||||||
|
|
||||||
SendForgotPasswordMail(ctx context.Context, email string) error
|
SendForgotPasswordMail(email string) error
|
||||||
ForgotPassword(ctx context.Context, token string, newPass string) error
|
ForgotPassword(token string, newPass string) error
|
||||||
|
|
||||||
IsCsrfTokenValid(ctx context.Context, tokenStr string, sessionId string) bool
|
IsCsrfTokenValid(tokenStr string, sessionId string) bool
|
||||||
GetCsrfToken(ctx context.Context, session *types.Session) (string, error)
|
GetCsrfToken(session *types.Session) (string, error)
|
||||||
|
|
||||||
CleanupSessionsAndTokens(ctx context.Context) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthImpl struct {
|
type AuthImpl struct {
|
||||||
@@ -66,8 +64,8 @@ func NewAuth(db db.Auth, random Random, clock Clock, mail Mail, serverSettings *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service AuthImpl) SignIn(ctx context.Context, session *types.Session, email string, password string) (*types.Session, *types.User, error) {
|
func (service AuthImpl) SignIn(session *types.Session, email string, password string) (*types.Session, *types.User, error) {
|
||||||
user, err := service.db.GetUserByEmail(ctx, email)
|
user, err := service.db.GetUserByEmail(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNotFound) {
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
return nil, nil, ErrInvalidCredentials
|
return nil, nil, ErrInvalidCredentials
|
||||||
@@ -82,49 +80,12 @@ func (service AuthImpl) SignIn(ctx context.Context, session *types.Session, emai
|
|||||||
return nil, nil, ErrInvalidCredentials
|
return nil, nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
newSession, err := service.createSession(ctx, user.Id)
|
err = service.cleanUpSessionWithTokens(session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, types.ErrInternal
|
return nil, nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
err = service.db.DeleteSession(ctx, session.Id)
|
session, err = service.createSession(user.Id)
|
||||||
if err != nil {
|
|
||||||
return nil, nil, types.ErrInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens, err := service.db.GetTokensBySessionIdAndType(ctx, session.Id, types.TokenTypeCsrf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, types.ErrInternal
|
|
||||||
}
|
|
||||||
for _, token := range tokens {
|
|
||||||
err = service.db.DeleteToken(ctx, token.Token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, types.ErrInternal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newSession, user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service AuthImpl) SignInSession(ctx context.Context, sessionId string) (*types.Session, *types.User, error) {
|
|
||||||
if sessionId == "" {
|
|
||||||
return nil, nil, ErrSessionIdInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
session, err := service.db.GetSession(ctx, sessionId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, types.ErrInternal
|
|
||||||
}
|
|
||||||
if session.ExpiresAt.Before(service.clock.Now()) {
|
|
||||||
_ = service.db.DeleteSession(ctx, sessionId)
|
|
||||||
return nil, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if session.UserId == uuid.Nil {
|
|
||||||
return session, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := service.db.GetUser(ctx, session.UserId)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, types.ErrInternal
|
return nil, nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
@@ -132,18 +93,44 @@ func (service AuthImpl) SignInSession(ctx context.Context, sessionId string) (*t
|
|||||||
return session, user, nil
|
return session, user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service AuthImpl) SignInAnonymous(ctx context.Context) (*types.Session, error) {
|
func (service AuthImpl) SignInSession(sessionId string) (*types.Session, *types.User, error) {
|
||||||
session, err := service.createSession(ctx, uuid.Nil)
|
if sessionId == "" {
|
||||||
|
return nil, nil, ErrSessionIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := service.db.GetSession(sessionId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, types.ErrInternal
|
||||||
|
}
|
||||||
|
if session.ExpiresAt.Before(service.clock.Now()) {
|
||||||
|
_ = service.db.DeleteSession(sessionId)
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.UserId == uuid.Nil {
|
||||||
|
return session, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := service.db.GetUser(session.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
return session, user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service AuthImpl) SignInAnonymous() (*types.Session, error) {
|
||||||
|
session, err := service.createSession(uuid.Nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.InfoContext(ctx, "anonymous session created", "session-id", session.Id)
|
slog.Info("anonymous session created", "session-id", session.Id)
|
||||||
|
|
||||||
return session, nil
|
return session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service AuthImpl) SignUp(ctx context.Context, email string, password string) (*types.User, error) {
|
func (service AuthImpl) SignUp(email string, password string) (*types.User, error) {
|
||||||
_, err := mail.ParseAddress(email)
|
_, err := mail.ParseAddress(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidEmail
|
return nil, ErrInvalidEmail
|
||||||
@@ -153,12 +140,12 @@ func (service AuthImpl) SignUp(ctx context.Context, email string, password strin
|
|||||||
return nil, ErrInvalidPassword
|
return nil, ErrInvalidPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
userId, err := service.random.UUID(ctx)
|
userId, err := service.random.UUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
salt, err := service.random.Bytes(ctx, 16)
|
salt, err := service.random.Bytes(16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
@@ -167,7 +154,7 @@ func (service AuthImpl) SignUp(ctx context.Context, email string, password strin
|
|||||||
|
|
||||||
user := types.NewUser(userId, email, false, nil, false, hash, salt, service.clock.Now())
|
user := types.NewUser(userId, email, false, nil, false, hash, salt, service.clock.Now())
|
||||||
|
|
||||||
err = service.db.InsertUser(ctx, user)
|
err = service.db.InsertUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrAlreadyExists) {
|
if errors.Is(err, db.ErrAlreadyExists) {
|
||||||
return nil, ErrAccountExists
|
return nil, ErrAccountExists
|
||||||
@@ -179,8 +166,8 @@ func (service AuthImpl) SignUp(ctx context.Context, email string, password strin
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service AuthImpl) SendVerificationMail(ctx context.Context, userId uuid.UUID, email string) {
|
func (service AuthImpl) SendVerificationMail(userId uuid.UUID, email string) {
|
||||||
tokens, err := service.db.GetTokensByUserIdAndType(ctx, userId, types.TokenTypeEmailVerify)
|
tokens, err := service.db.GetTokensByUserIdAndType(userId, types.TokenTypeEmailVerify)
|
||||||
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -192,7 +179,7 @@ func (service AuthImpl) SendVerificationMail(ctx context.Context, userId uuid.UU
|
|||||||
}
|
}
|
||||||
|
|
||||||
if token == nil {
|
if token == nil {
|
||||||
newTokenStr, err := service.random.String(ctx, 32)
|
newTokenStr, err := service.random.String(32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -205,7 +192,7 @@ func (service AuthImpl) SendVerificationMail(ctx context.Context, userId uuid.UU
|
|||||||
service.clock.Now(),
|
service.clock.Now(),
|
||||||
service.clock.Now().Add(24*time.Hour))
|
service.clock.Now().Add(24*time.Hour))
|
||||||
|
|
||||||
err = service.db.InsertToken(ctx, token)
|
err = service.db.InsertToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -214,24 +201,24 @@ func (service AuthImpl) SendVerificationMail(ctx context.Context, userId uuid.UU
|
|||||||
var w strings.Builder
|
var w strings.Builder
|
||||||
err = mailTemplate.Register(service.serverSettings.BaseUrl, token.Token).Render(context.Background(), &w)
|
err = mailTemplate.Register(service.serverSettings.BaseUrl, token.Token).Render(context.Background(), &w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not render welcome email", "err", err)
|
slog.Error("Could not render welcome email", "err", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
service.mail.SendMail(ctx, email, "Welcome to spend-sparrow", w.String())
|
service.mail.SendMail(email, "Welcome to spend-sparrow", w.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service AuthImpl) VerifyUserEmail(ctx context.Context, tokenStr string) error {
|
func (service AuthImpl) VerifyUserEmail(tokenStr string) error {
|
||||||
if tokenStr == "" {
|
if tokenStr == "" {
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := service.db.GetToken(ctx, tokenStr)
|
token, err := service.db.GetToken(tokenStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := service.db.GetUser(ctx, token.UserId)
|
user, err := service.db.GetUser(token.UserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
@@ -249,21 +236,21 @@ func (service AuthImpl) VerifyUserEmail(ctx context.Context, tokenStr string) er
|
|||||||
user.EmailVerified = true
|
user.EmailVerified = true
|
||||||
user.EmailVerifiedAt = &now
|
user.EmailVerifiedAt = &now
|
||||||
|
|
||||||
err = service.db.UpdateUser(ctx, user)
|
err = service.db.UpdateUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = service.db.DeleteToken(ctx, token.Token)
|
_ = service.db.DeleteToken(token.Token)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service AuthImpl) SignOut(ctx context.Context, sessionId string) error {
|
func (service AuthImpl) SignOut(sessionId string) error {
|
||||||
return service.db.DeleteSession(ctx, sessionId)
|
return service.db.DeleteSession(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service AuthImpl) DeleteAccount(ctx context.Context, user *types.User, currPass string) error {
|
func (service AuthImpl) DeleteAccount(user *types.User, currPass string) error {
|
||||||
userDb, err := service.db.GetUser(ctx, user.Id)
|
userDb, err := service.db.GetUser(user.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
@@ -273,17 +260,17 @@ func (service AuthImpl) DeleteAccount(ctx context.Context, user *types.User, cur
|
|||||||
return ErrInvalidCredentials
|
return ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
err = service.db.DeleteUser(ctx, user.Id)
|
err = service.db.DeleteUser(user.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
service.mail.SendMail(ctx, user.Email, "Account deleted", "Your account has been deleted")
|
service.mail.SendMail(user.Email, "Account deleted", "Your account has been deleted")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service AuthImpl) ChangePassword(ctx context.Context, user *types.User, sessionId string, currPass, newPass string) error {
|
func (service AuthImpl) ChangePassword(user *types.User, sessionId string, currPass, newPass string) error {
|
||||||
if !isPasswordValid(newPass) {
|
if !isPasswordValid(newPass) {
|
||||||
return ErrInvalidPassword
|
return ErrInvalidPassword
|
||||||
}
|
}
|
||||||
@@ -301,18 +288,18 @@ func (service AuthImpl) ChangePassword(ctx context.Context, user *types.User, se
|
|||||||
newHash := GetHashPassword(newPass, user.Salt)
|
newHash := GetHashPassword(newPass, user.Salt)
|
||||||
user.Password = newHash
|
user.Password = newHash
|
||||||
|
|
||||||
err := service.db.UpdateUser(ctx, user)
|
err := service.db.UpdateUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions, err := service.db.GetSessions(ctx, user.Id)
|
sessions, err := service.db.GetSessions(user.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
for _, s := range sessions {
|
for _, s := range sessions {
|
||||||
if s.Id != sessionId {
|
if s.Id != sessionId {
|
||||||
err = service.db.DeleteSession(ctx, s.Id)
|
err = service.db.DeleteSession(s.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
@@ -322,13 +309,13 @@ func (service AuthImpl) ChangePassword(ctx context.Context, user *types.User, se
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service AuthImpl) SendForgotPasswordMail(ctx context.Context, email string) error {
|
func (service AuthImpl) SendForgotPasswordMail(email string) error {
|
||||||
tokenStr, err := service.random.String(ctx, 32)
|
tokenStr, err := service.random.String(32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := service.db.GetUserByEmail(ctx, email)
|
user, err := service.db.GetUserByEmail(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNotFound) {
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
return nil
|
return nil
|
||||||
@@ -345,7 +332,7 @@ func (service AuthImpl) SendForgotPasswordMail(ctx context.Context, email string
|
|||||||
service.clock.Now(),
|
service.clock.Now(),
|
||||||
service.clock.Now().Add(15*time.Minute))
|
service.clock.Now().Add(15*time.Minute))
|
||||||
|
|
||||||
err = service.db.InsertToken(ctx, token)
|
err = service.db.InsertToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
@@ -353,25 +340,25 @@ func (service AuthImpl) SendForgotPasswordMail(ctx context.Context, email string
|
|||||||
var mail strings.Builder
|
var mail strings.Builder
|
||||||
err = mailTemplate.ResetPassword(service.serverSettings.BaseUrl, token.Token).Render(context.Background(), &mail)
|
err = mailTemplate.ResetPassword(service.serverSettings.BaseUrl, token.Token).Render(context.Background(), &mail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not render reset password email", "err", err)
|
slog.Error("Could not render reset password email", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
service.mail.SendMail(ctx, email, "Reset Password", mail.String())
|
service.mail.SendMail(email, "Reset Password", mail.String())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service AuthImpl) ForgotPassword(ctx context.Context, tokenStr string, newPass string) error {
|
func (service AuthImpl) ForgotPassword(tokenStr string, newPass string) error {
|
||||||
if !isPasswordValid(newPass) {
|
if !isPasswordValid(newPass) {
|
||||||
return ErrInvalidPassword
|
return ErrInvalidPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := service.db.GetToken(ctx, tokenStr)
|
token, err := service.db.GetToken(tokenStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrTokenInvalid
|
return ErrTokenInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
err = service.db.DeleteToken(ctx, tokenStr)
|
err = service.db.DeleteToken(tokenStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -381,27 +368,27 @@ func (service AuthImpl) ForgotPassword(ctx context.Context, tokenStr string, new
|
|||||||
return ErrTokenInvalid
|
return ErrTokenInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := service.db.GetUser(ctx, token.UserId)
|
user, err := service.db.GetUser(token.UserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not get user from token", "err", err)
|
slog.Error("Could not get user from token", "err", err)
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
passHash := GetHashPassword(newPass, user.Salt)
|
passHash := GetHashPassword(newPass, user.Salt)
|
||||||
|
|
||||||
user.Password = passHash
|
user.Password = passHash
|
||||||
err = service.db.UpdateUser(ctx, user)
|
err = service.db.UpdateUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions, err := service.db.GetSessions(ctx, user.Id)
|
sessions, err := service.db.GetSessions(user.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, session := range sessions {
|
for _, session := range sessions {
|
||||||
err = service.db.DeleteSession(ctx, session.Id)
|
err = service.db.DeleteSession(session.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
@@ -410,8 +397,8 @@ func (service AuthImpl) ForgotPassword(ctx context.Context, tokenStr string, new
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service AuthImpl) IsCsrfTokenValid(ctx context.Context, tokenStr string, sessionId string) bool {
|
func (service AuthImpl) IsCsrfTokenValid(tokenStr string, sessionId string) bool {
|
||||||
token, err := service.db.GetToken(ctx, tokenStr)
|
token, err := service.db.GetToken(tokenStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -425,18 +412,18 @@ func (service AuthImpl) IsCsrfTokenValid(ctx context.Context, tokenStr string, s
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service AuthImpl) GetCsrfToken(ctx context.Context, session *types.Session) (string, error) {
|
func (service AuthImpl) GetCsrfToken(session *types.Session) (string, error) {
|
||||||
if session == nil {
|
if session == nil {
|
||||||
return "", types.ErrInternal
|
return "", types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens, _ := service.db.GetTokensBySessionIdAndType(ctx, session.Id, types.TokenTypeCsrf)
|
tokens, _ := service.db.GetTokensBySessionIdAndType(session.Id, types.TokenTypeCsrf)
|
||||||
|
|
||||||
if len(tokens) > 0 {
|
if len(tokens) > 0 {
|
||||||
return tokens[0].Token, nil
|
return tokens[0].Token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenStr, err := service.random.String(ctx, 32)
|
tokenStr, err := service.random.String(32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", types.ErrInternal
|
return "", types.ErrInternal
|
||||||
}
|
}
|
||||||
@@ -448,32 +435,47 @@ func (service AuthImpl) GetCsrfToken(ctx context.Context, session *types.Session
|
|||||||
types.TokenTypeCsrf,
|
types.TokenTypeCsrf,
|
||||||
service.clock.Now(),
|
service.clock.Now(),
|
||||||
service.clock.Now().Add(8*time.Hour))
|
service.clock.Now().Add(8*time.Hour))
|
||||||
err = service.db.InsertToken(ctx, token)
|
err = service.db.InsertToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", types.ErrInternal
|
return "", types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.InfoContext(ctx, "CSRF-Token created", "token", tokenStr)
|
slog.Info("CSRF-Token created", "token", tokenStr)
|
||||||
|
|
||||||
return tokenStr, nil
|
return tokenStr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service AuthImpl) CleanupSessionsAndTokens(ctx context.Context) error {
|
func (service AuthImpl) cleanUpSessionWithTokens(session *types.Session) error {
|
||||||
err := service.db.DeleteOldSessions(ctx)
|
if session == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := service.db.DeleteSession(session.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
err = service.db.DeleteOldTokens(ctx)
|
tokens, err := service.db.GetTokensBySessionIdAndType(session.Id, types.TokenTypeCsrf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ErrInternal
|
return types.ErrInternal
|
||||||
}
|
}
|
||||||
|
for _, token := range tokens {
|
||||||
|
err = service.db.DeleteToken(token.Token)
|
||||||
|
if err != nil {
|
||||||
|
return types.ErrInternal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service AuthImpl) createSession(ctx context.Context, userId uuid.UUID) (*types.Session, error) {
|
func (service AuthImpl) createSession(userId uuid.UUID) (*types.Session, error) {
|
||||||
sessionId, err := service.random.String(ctx, 32)
|
sessionId, err := service.random.String(32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.db.DeleteOldSessions(userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
@@ -483,7 +485,7 @@ func (service AuthImpl) createSession(ctx context.Context, userId uuid.UUID) (*t
|
|||||||
|
|
||||||
session := types.NewSession(sessionId, userId, createAt, expiresAt)
|
session := types.NewSession(sessionId, userId, createAt, expiresAt)
|
||||||
|
|
||||||
err = service.db.InsertSession(ctx, session)
|
err = service.db.InsertSession(session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ func NewClock() Clock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClockImpl) Now() time.Time {
|
func (c *ClockImpl) Now() time.Time {
|
||||||
return time.Now().UTC()
|
return time.Now()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"spend-sparrow/internal/db"
|
|
||||||
"spend-sparrow/internal/types"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Dashboard struct {
|
|
||||||
db *sqlx.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDashboard(db *sqlx.DB) *Dashboard {
|
|
||||||
return &Dashboard{
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Dashboard) MainChart(
|
|
||||||
ctx context.Context,
|
|
||||||
user *types.User,
|
|
||||||
) ([]types.DashboardMainChartEntry, error) {
|
|
||||||
if user == nil {
|
|
||||||
return nil, ErrUnauthorized
|
|
||||||
}
|
|
||||||
|
|
||||||
transactions := make([]types.Transaction, 0)
|
|
||||||
err := s.db.SelectContext(ctx, &transactions, `
|
|
||||||
SELECT *
|
|
||||||
FROM "transaction"
|
|
||||||
WHERE user_id = ?
|
|
||||||
ORDER BY timestamp`, user.Id)
|
|
||||||
err = db.TransformAndLogDbError(ctx, "dashboard Chart", nil, err)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
timeEntries := make([]types.DashboardMainChartEntry, 0)
|
|
||||||
|
|
||||||
var lastEntry *types.DashboardMainChartEntry
|
|
||||||
|
|
||||||
for _, t := range transactions {
|
|
||||||
if t.Error != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
newDay := t.Timestamp.Truncate(24 * time.Hour)
|
|
||||||
if lastEntry == nil {
|
|
||||||
lastEntry = &types.DashboardMainChartEntry{
|
|
||||||
Day: newDay,
|
|
||||||
Value: 0,
|
|
||||||
Savings: 0,
|
|
||||||
}
|
|
||||||
} else if lastEntry.Day != newDay {
|
|
||||||
timeEntries = append(timeEntries, *lastEntry)
|
|
||||||
lastEntry = &types.DashboardMainChartEntry{
|
|
||||||
Day: newDay,
|
|
||||||
Value: lastEntry.Value,
|
|
||||||
Savings: lastEntry.Savings,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.AccountId != nil {
|
|
||||||
lastEntry.Value += t.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.TreasureChestId != nil {
|
|
||||||
lastEntry.Savings += t.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastEntry != nil {
|
|
||||||
timeEntries = append(timeEntries, *lastEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
return timeEntries, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Dashboard) TreasureChests(
|
|
||||||
ctx context.Context,
|
|
||||||
user *types.User,
|
|
||||||
) ([]*types.DashboardTreasureChest, error) {
|
|
||||||
if user == nil {
|
|
||||||
return nil, ErrUnauthorized
|
|
||||||
}
|
|
||||||
|
|
||||||
treasureChests := make([]*types.TreasureChest, 0)
|
|
||||||
err := s.db.SelectContext(ctx, &treasureChests, `SELECT * FROM treasure_chest WHERE user_id = ?`, user.Id)
|
|
||||||
err = db.TransformAndLogDbError(ctx, "dashboard TreasureChests", nil, err)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
treasureChests = sortTreasureChests(treasureChests)
|
|
||||||
|
|
||||||
result := make([]*types.DashboardTreasureChest, 0)
|
|
||||||
|
|
||||||
for _, t := range treasureChests {
|
|
||||||
if t.ParentId == nil {
|
|
||||||
result = append(result, &types.DashboardTreasureChest{
|
|
||||||
Name: t.Name,
|
|
||||||
Value: t.CurrentBalance,
|
|
||||||
Children: make([]types.DashboardTreasureChest, 0),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
result[len(result)-1].Children = append(result[len(result)-1].Children, types.DashboardTreasureChest{
|
|
||||||
Name: t.Name,
|
|
||||||
Value: t.CurrentBalance,
|
|
||||||
Children: make([]types.DashboardTreasureChest, 0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Dashboard) TreasureChest(
|
|
||||||
ctx context.Context,
|
|
||||||
user *types.User,
|
|
||||||
treausureChestId *uuid.UUID,
|
|
||||||
) ([]types.DashboardMainChartEntry, error) {
|
|
||||||
if user == nil {
|
|
||||||
return nil, ErrUnauthorized
|
|
||||||
}
|
|
||||||
|
|
||||||
transactions := make([]types.Transaction, 0)
|
|
||||||
err := s.db.SelectContext(ctx, &transactions, `
|
|
||||||
SELECT *
|
|
||||||
FROM "transaction"
|
|
||||||
WHERE user_id = ?
|
|
||||||
AND treasure_chest_id = ?
|
|
||||||
ORDER BY timestamp`, user.Id, treausureChestId)
|
|
||||||
err = db.TransformAndLogDbError(ctx, "dashboard Chart", nil, err)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
timeEntries := make([]types.DashboardMainChartEntry, 0)
|
|
||||||
|
|
||||||
var lastEntry *types.DashboardMainChartEntry
|
|
||||||
|
|
||||||
for _, t := range transactions {
|
|
||||||
if t.Error != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
newDay := t.Timestamp.Truncate(24 * time.Hour)
|
|
||||||
if lastEntry == nil {
|
|
||||||
lastEntry = &types.DashboardMainChartEntry{
|
|
||||||
Day: newDay,
|
|
||||||
Value: 0,
|
|
||||||
}
|
|
||||||
} else if lastEntry.Day != newDay {
|
|
||||||
timeEntries = append(timeEntries, *lastEntry)
|
|
||||||
lastEntry = &types.DashboardMainChartEntry{
|
|
||||||
Day: newDay,
|
|
||||||
Value: lastEntry.Value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.TreasureChestId != nil {
|
|
||||||
lastEntry.Value += t.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastEntry != nil {
|
|
||||||
timeEntries = append(timeEntries, *lastEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
return timeEntries, nil
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
safeInputRegex = regexp.MustCompile(`^[a-zA-Z0-9ÄÖÜäöüß,&'". \-\?]+$`)
|
safeInputRegex = regexp.MustCompile(`^[a-zA-Z0-9ÄÖÜäöüß,&'" -]+$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
func validateString(value string, fieldName string) error {
|
func validateString(value string, fieldName string) error {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
@@ -10,7 +9,7 @@ import (
|
|||||||
|
|
||||||
type Mail interface {
|
type Mail interface {
|
||||||
// Sending an email is a fire and forget operation. Thus no error handling
|
// Sending an email is a fire and forget operation. Thus no error handling
|
||||||
SendMail(ctx context.Context, to string, subject string, message string)
|
SendMail(to string, subject string, message string)
|
||||||
}
|
}
|
||||||
|
|
||||||
type MailImpl struct {
|
type MailImpl struct {
|
||||||
@@ -21,11 +20,11 @@ func NewMail(server *types.Settings) MailImpl {
|
|||||||
return MailImpl{server: server}
|
return MailImpl{server: server}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m MailImpl) SendMail(ctx context.Context, to string, subject string, message string) {
|
func (m MailImpl) SendMail(to string, subject string, message string) {
|
||||||
go m.internalSendMail(ctx, to, subject, message)
|
go m.internalSendMail(to, subject, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m MailImpl) internalSendMail(ctx context.Context, to string, subject string, message string) {
|
func (m MailImpl) internalSendMail(to string, subject string, message string) {
|
||||||
if m.server.Smtp == nil {
|
if m.server.Smtp == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -48,9 +47,9 @@ func (m MailImpl) internalSendMail(ctx context.Context, to string, subject strin
|
|||||||
subject,
|
subject,
|
||||||
message)
|
message)
|
||||||
|
|
||||||
slog.InfoContext(ctx, "sending mail", "to", to)
|
slog.Info("sending mail", "to", to)
|
||||||
err := smtp.SendMail(s.Host+":"+s.Port, auth, s.FromMail, []string{to}, []byte(msg))
|
err := smtp.SendMail(s.Host+":"+s.Port, auth, s.FromMail, []string{to}, []byte(msg))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Error sending mail", "err", err)
|
slog.Error("Error sending mail", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -11,9 +10,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Random interface {
|
type Random interface {
|
||||||
Bytes(ctx context.Context, size int) ([]byte, error)
|
Bytes(size int) ([]byte, error)
|
||||||
String(ctx context.Context, size int) (string, error)
|
String(size int) (string, error)
|
||||||
UUID(ctx context.Context) (uuid.UUID, error)
|
UUID() (uuid.UUID, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RandomImpl struct {
|
type RandomImpl struct {
|
||||||
@@ -23,31 +22,31 @@ func NewRandom() *RandomImpl {
|
|||||||
return &RandomImpl{}
|
return &RandomImpl{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RandomImpl) Bytes(ctx context.Context, tsize int) ([]byte, error) {
|
func (r *RandomImpl) Bytes(size int) ([]byte, error) {
|
||||||
b := make([]byte, 32)
|
b := make([]byte, 32)
|
||||||
_, err := rand.Read(b)
|
_, err := rand.Read(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Error generating random bytes", "err", err)
|
slog.Error("Error generating random bytes", "err", err)
|
||||||
return []byte{}, types.ErrInternal
|
return []byte{}, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RandomImpl) String(ctx context.Context, size int) (string, error) {
|
func (r *RandomImpl) String(size int) (string, error) {
|
||||||
bytes, err := r.Bytes(ctx, size)
|
bytes, err := r.Bytes(size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Error generating random string", "err", err)
|
slog.Error("Error generating random string", "err", err)
|
||||||
return "", types.ErrInternal
|
return "", types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
return base64.StdEncoding.EncodeToString(bytes), nil
|
return base64.StdEncoding.EncodeToString(bytes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RandomImpl) UUID(ctx context.Context) (uuid.UUID, error) {
|
func (r *RandomImpl) UUID() (uuid.UUID, error) {
|
||||||
id, err := uuid.NewRandom()
|
id, err := uuid.NewRandom()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Error generating random UUID", "err", err)
|
slog.Error("Error generating random UUID", "err", err)
|
||||||
return uuid.Nil, types.ErrInternal
|
return uuid.Nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,25 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"spend-sparrow/internal/db"
|
"spend-sparrow/internal/db"
|
||||||
"spend-sparrow/internal/types"
|
"spend-sparrow/internal/types"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
const page_size = 25
|
|
||||||
|
|
||||||
type Transaction interface {
|
type Transaction interface {
|
||||||
Add(ctx context.Context, tx *sqlx.Tx, user *types.User, transaction types.Transaction) (*types.Transaction, error)
|
Add(tx *sqlx.Tx, user *types.User, transaction types.Transaction) (*types.Transaction, error)
|
||||||
Update(ctx context.Context, user *types.User, transaction types.Transaction) (*types.Transaction, error)
|
Update(user *types.User, transaction types.Transaction) (*types.Transaction, error)
|
||||||
Get(ctx context.Context, user *types.User, id string) (*types.Transaction, error)
|
Get(user *types.User, id string) (*types.Transaction, error)
|
||||||
GetAll(ctx context.Context, user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error)
|
GetAll(user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error)
|
||||||
Delete(ctx context.Context, user *types.User, id string) error
|
Delete(user *types.User, id string) error
|
||||||
|
|
||||||
RecalculateBalances(ctx context.Context, user *types.User) error
|
RecalculateBalances(user *types.User) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransactionImpl struct {
|
type TransactionImpl struct {
|
||||||
@@ -40,7 +36,7 @@ func NewTransaction(db *sqlx.DB, random Random, clock Clock) Transaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) Add(ctx context.Context, tx *sqlx.Tx, user *types.User, transactionInput types.Transaction) (*types.Transaction, error) {
|
func (s TransactionImpl) Add(tx *sqlx.Tx, user *types.User, transactionInput types.Transaction) (*types.Transaction, error) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
@@ -49,8 +45,8 @@ func (s TransactionImpl) Add(ctx context.Context, tx *sqlx.Tx, user *types.User,
|
|||||||
ownsTransaction := false
|
ownsTransaction := false
|
||||||
if tx == nil {
|
if tx == nil {
|
||||||
ownsTransaction = true
|
ownsTransaction = true
|
||||||
tx, err = s.db.BeginTxx(ctx, nil)
|
tx, err = s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Add", nil, err)
|
err = db.TransformAndLogDbError("transaction Add", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -59,38 +55,38 @@ func (s TransactionImpl) Add(ctx context.Context, tx *sqlx.Tx, user *types.User,
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction, err := s.validateAndEnrichTransaction(ctx, tx, nil, user.Id, transactionInput)
|
transaction, err := s.validateAndEnrichTransaction(tx, nil, user.Id, transactionInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := tx.NamedExecContext(ctx, `
|
r, err := tx.NamedExec(`
|
||||||
INSERT INTO "transaction" (id, user_id, account_id, treasure_chest_id, value, timestamp,
|
INSERT INTO "transaction" (id, user_id, account_id, treasure_chest_id, value, timestamp,
|
||||||
party, description, error, created_at, created_by)
|
party, description, error, created_at, created_by)
|
||||||
VALUES (:id, :user_id, :account_id, :treasure_chest_id, :value, :timestamp,
|
VALUES (:id, :user_id, :account_id, :treasure_chest_id, :value, :timestamp,
|
||||||
:party, :description, :error, :created_at, :created_by)`, transaction)
|
:party, :description, :error, :created_at, :created_by)`, transaction)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Insert", r, err)
|
err = db.TransformAndLogDbError("transaction Insert", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if transaction.Error == nil && transaction.AccountId != nil {
|
if transaction.Error == nil && transaction.AccountId != nil {
|
||||||
r, err = tx.ExecContext(ctx, `
|
r, err = tx.Exec(`
|
||||||
UPDATE account
|
UPDATE account
|
||||||
SET current_balance = current_balance + ?
|
SET current_balance = current_balance + ?
|
||||||
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
|
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Add", r, err)
|
err = db.TransformAndLogDbError("transaction Add", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if transaction.Error == nil && transaction.TreasureChestId != nil {
|
if transaction.Error == nil && transaction.TreasureChestId != nil {
|
||||||
r, err = tx.ExecContext(ctx, `
|
r, err = tx.Exec(`
|
||||||
UPDATE treasure_chest
|
UPDATE treasure_chest
|
||||||
SET current_balance = current_balance + ?
|
SET current_balance = current_balance + ?
|
||||||
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
|
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Add", r, err)
|
err = db.TransformAndLogDbError("transaction Add", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -98,7 +94,7 @@ func (s TransactionImpl) Add(ctx context.Context, tx *sqlx.Tx, user *types.User,
|
|||||||
|
|
||||||
if ownsTransaction {
|
if ownsTransaction {
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Add", nil, err)
|
err = db.TransformAndLogDbError("transaction Add", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -107,13 +103,13 @@ func (s TransactionImpl) Add(ctx context.Context, tx *sqlx.Tx, user *types.User,
|
|||||||
return transaction, nil
|
return transaction, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) Update(ctx context.Context, user *types.User, input types.Transaction) (*types.Transaction, error) {
|
func (s TransactionImpl) Update(user *types.User, input types.Transaction) (*types.Transaction, error) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := s.db.BeginTxx(ctx, nil)
|
tx, err := s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Update", nil, err)
|
err = db.TransformAndLogDbError("transaction Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -122,8 +118,8 @@ func (s TransactionImpl) Update(ctx context.Context, user *types.User, input typ
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
transaction := &types.Transaction{}
|
transaction := &types.Transaction{}
|
||||||
err = tx.GetContext(ctx, transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, input.Id)
|
err = tx.Get(transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, input.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Update", nil, err)
|
err = db.TransformAndLogDbError("transaction Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNotFound) {
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
return nil, fmt.Errorf("transaction %v not found: %w", input.Id, ErrBadRequest)
|
return nil, fmt.Errorf("transaction %v not found: %w", input.Id, ErrBadRequest)
|
||||||
@@ -132,53 +128,53 @@ func (s TransactionImpl) Update(ctx context.Context, user *types.User, input typ
|
|||||||
}
|
}
|
||||||
|
|
||||||
if transaction.Error == nil && transaction.AccountId != nil {
|
if transaction.Error == nil && transaction.AccountId != nil {
|
||||||
r, err := tx.ExecContext(ctx, `
|
r, err := tx.Exec(`
|
||||||
UPDATE account
|
UPDATE account
|
||||||
SET current_balance = current_balance - ?
|
SET current_balance = current_balance - ?
|
||||||
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
|
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Update", r, err)
|
err = db.TransformAndLogDbError("transaction Update", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if transaction.Error == nil && transaction.TreasureChestId != nil {
|
if transaction.Error == nil && transaction.TreasureChestId != nil {
|
||||||
r, err := tx.ExecContext(ctx, `
|
r, err := tx.Exec(`
|
||||||
UPDATE treasure_chest
|
UPDATE treasure_chest
|
||||||
SET current_balance = current_balance - ?
|
SET current_balance = current_balance - ?
|
||||||
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
|
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Update", r, err)
|
err = db.TransformAndLogDbError("transaction Update", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction, err = s.validateAndEnrichTransaction(ctx, tx, transaction, user.Id, input)
|
transaction, err = s.validateAndEnrichTransaction(tx, transaction, user.Id, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if transaction.Error == nil && transaction.AccountId != nil {
|
if transaction.Error == nil && transaction.AccountId != nil {
|
||||||
r, err := tx.ExecContext(ctx, `
|
r, err := tx.Exec(`
|
||||||
UPDATE account
|
UPDATE account
|
||||||
SET current_balance = current_balance + ?
|
SET current_balance = current_balance + ?
|
||||||
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
|
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Update", r, err)
|
err = db.TransformAndLogDbError("transaction Update", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if transaction.Error == nil && transaction.TreasureChestId != nil {
|
if transaction.Error == nil && transaction.TreasureChestId != nil {
|
||||||
r, err := tx.ExecContext(ctx, `
|
r, err := tx.Exec(`
|
||||||
UPDATE treasure_chest
|
UPDATE treasure_chest
|
||||||
SET current_balance = current_balance + ?
|
SET current_balance = current_balance + ?
|
||||||
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
|
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Update", r, err)
|
err = db.TransformAndLogDbError("transaction Update", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := tx.NamedExecContext(ctx, `
|
r, err := tx.NamedExec(`
|
||||||
UPDATE "transaction"
|
UPDATE "transaction"
|
||||||
SET
|
SET
|
||||||
account_id = :account_id,
|
account_id = :account_id,
|
||||||
@@ -192,13 +188,13 @@ func (s TransactionImpl) Update(ctx context.Context, user *types.User, input typ
|
|||||||
updated_by = :updated_by
|
updated_by = :updated_by
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
AND user_id = :user_id`, transaction)
|
AND user_id = :user_id`, transaction)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Update", r, err)
|
err = db.TransformAndLogDbError("transaction Update", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Update", nil, err)
|
err = db.TransformAndLogDbError("transaction Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -206,19 +202,19 @@ func (s TransactionImpl) Update(ctx context.Context, user *types.User, input typ
|
|||||||
return transaction, nil
|
return transaction, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) Get(ctx context.Context, user *types.User, id string) (*types.Transaction, error) {
|
func (s TransactionImpl) Get(user *types.User, id string) (*types.Transaction, error) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
uuid, err := uuid.Parse(id)
|
uuid, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "transaction get", "err", err)
|
slog.Error("transaction get", "err", err)
|
||||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
var transaction types.Transaction
|
var transaction types.Transaction
|
||||||
err = s.db.GetContext(ctx, &transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
err = s.db.Get(&transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Get", nil, err)
|
err = db.TransformAndLogDbError("transaction Get", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNotFound) {
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
return nil, fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest)
|
return nil, fmt.Errorf("transaction %v not found: %w", id, ErrBadRequest)
|
||||||
@@ -229,47 +225,28 @@ func (s TransactionImpl) Get(ctx context.Context, user *types.User, id string) (
|
|||||||
return &transaction, nil
|
return &transaction, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) GetAll(ctx context.Context, user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error) {
|
func (s TransactionImpl) GetAll(user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
page int64
|
|
||||||
offset int64
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if filter.Page != "" {
|
|
||||||
page, err = strconv.ParseInt(filter.Page, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
offset = 0
|
|
||||||
} else {
|
|
||||||
offset = page - 1
|
|
||||||
offset *= page_size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transactions := make([]*types.Transaction, 0)
|
transactions := make([]*types.Transaction, 0)
|
||||||
err = s.db.SelectContext(ctx, &transactions, `
|
err := s.db.Select(&transactions, `
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM "transaction"
|
FROM "transaction"
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
AND ($1 = '' OR account_id = $1)
|
AND (? = '' OR account_id = ?)
|
||||||
AND ($2 = '' OR treasure_chest_id = $2)
|
AND (? = '' OR treasure_chest_id = ?)
|
||||||
AND ($3 = ''
|
AND (? = ''
|
||||||
OR ($3 = "true" AND error IS NOT NULL)
|
OR (? = "true" AND error IS NOT NULL)
|
||||||
OR ($3 = "false" AND error IS NULL)
|
OR (? = "false" AND error IS NULL)
|
||||||
)
|
)
|
||||||
ORDER BY timestamp DESC, created_at DESC
|
ORDER BY timestamp DESC, created_at DESC`,
|
||||||
LIMIT $4 OFFSET $5
|
|
||||||
`,
|
|
||||||
user.Id,
|
user.Id,
|
||||||
filter.AccountId,
|
filter.AccountId, filter.AccountId,
|
||||||
filter.TreasureChestId,
|
filter.TreasureChestId, filter.TreasureChestId,
|
||||||
filter.Error,
|
filter.Error, filter.Error, filter.Error)
|
||||||
page_size,
|
err = db.TransformAndLogDbError("transaction GetAll", nil, err)
|
||||||
offset)
|
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction GetAll", nil, err)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -277,18 +254,18 @@ func (s TransactionImpl) GetAll(ctx context.Context, user *types.User, filter ty
|
|||||||
return transactions, nil
|
return transactions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) Delete(ctx context.Context, user *types.User, id string) error {
|
func (s TransactionImpl) Delete(user *types.User, id string) error {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return ErrUnauthorized
|
return ErrUnauthorized
|
||||||
}
|
}
|
||||||
uuid, err := uuid.Parse(id)
|
uuid, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "transaction delete", "err", err)
|
slog.Error("transaction delete", "err", err)
|
||||||
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := s.db.BeginTxx(ctx, nil)
|
tx, err := s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Delete", nil, err)
|
err = db.TransformAndLogDbError("transaction Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -297,44 +274,44 @@ func (s TransactionImpl) Delete(ctx context.Context, user *types.User, id string
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
var transaction types.Transaction
|
var transaction types.Transaction
|
||||||
err = tx.GetContext(ctx, &transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
err = tx.Get(&transaction, `SELECT * FROM "transaction" WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Delete", nil, err)
|
err = db.TransformAndLogDbError("transaction Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if transaction.Error == nil && transaction.AccountId != nil {
|
if transaction.Error == nil && transaction.AccountId != nil {
|
||||||
r, err := tx.ExecContext(ctx, `
|
r, err := tx.Exec(`
|
||||||
UPDATE account
|
UPDATE account
|
||||||
SET current_balance = current_balance - ?
|
SET current_balance = current_balance - ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
|
AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Delete", r, err)
|
err = db.TransformAndLogDbError("transaction Delete", r, err)
|
||||||
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if transaction.Error == nil && transaction.TreasureChestId != nil {
|
if transaction.Error == nil && transaction.TreasureChestId != nil {
|
||||||
r, err := tx.ExecContext(ctx, `
|
r, err := tx.Exec(`
|
||||||
UPDATE treasure_chest
|
UPDATE treasure_chest
|
||||||
SET current_balance = current_balance - ?
|
SET current_balance = current_balance - ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
|
AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Delete", r, err)
|
err = db.TransformAndLogDbError("transaction Delete", r, err)
|
||||||
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := tx.ExecContext(ctx, "DELETE FROM \"transaction\" WHERE id = ? AND user_id = ?", uuid, user.Id)
|
r, err := tx.Exec("DELETE FROM \"transaction\" WHERE id = ? AND user_id = ?", uuid, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Delete", r, err)
|
err = db.TransformAndLogDbError("transaction Delete", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction Delete", nil, err)
|
err = db.TransformAndLogDbError("transaction Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -342,13 +319,13 @@ func (s TransactionImpl) Delete(ctx context.Context, user *types.User, id string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) RecalculateBalances(ctx context.Context, user *types.User) error {
|
func (s TransactionImpl) RecalculateBalances(user *types.User) error {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return ErrUnauthorized
|
return ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := s.db.BeginTxx(ctx, nil)
|
tx, err := s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", nil, err)
|
err = db.TransformAndLogDbError("transaction RecalculateBalances", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -356,54 +333,54 @@ func (s TransactionImpl) RecalculateBalances(ctx context.Context, user *types.Us
|
|||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
r, err := tx.ExecContext(ctx, `
|
r, err := tx.Exec(`
|
||||||
UPDATE account
|
UPDATE account
|
||||||
SET current_balance = 0
|
SET current_balance = 0
|
||||||
WHERE user_id = ?`, user.Id)
|
WHERE user_id = ?`, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", r, err)
|
err = db.TransformAndLogDbError("transaction RecalculateBalances", r, err)
|
||||||
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err = tx.ExecContext(ctx, `
|
r, err = tx.Exec(`
|
||||||
UPDATE treasure_chest
|
UPDATE treasure_chest
|
||||||
SET current_balance = 0
|
SET current_balance = 0
|
||||||
WHERE user_id = ?`, user.Id)
|
WHERE user_id = ?`, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", r, err)
|
err = db.TransformAndLogDbError("transaction RecalculateBalances", r, err)
|
||||||
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := tx.QueryxContext(ctx, `
|
rows, err := tx.Queryx(`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM "transaction"
|
FROM "transaction"
|
||||||
WHERE user_id = ?`, user.Id)
|
WHERE user_id = ?`, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", nil, err)
|
err = db.TransformAndLogDbError("transaction RecalculateBalances", nil, err)
|
||||||
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
err := rows.Close()
|
err := rows.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "transaction RecalculateBalances", "err", err)
|
slog.Error("transaction RecalculateBalances", "err", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var transaction types.Transaction
|
var transaction types.Transaction
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
err = rows.StructScan(&transaction)
|
err = rows.StructScan(&transaction)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", nil, err)
|
err = db.TransformAndLogDbError("transaction RecalculateBalances", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.updateErrors(&transaction)
|
s.updateErrors(&transaction)
|
||||||
r, err = tx.ExecContext(ctx, `
|
r, err = tx.Exec(`
|
||||||
UPDATE "transaction"
|
UPDATE "transaction"
|
||||||
SET error = ?
|
SET error = ?
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
AND id = ?`, transaction.Error, user.Id, transaction.Id)
|
AND id = ?`, transaction.Error, user.Id, transaction.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", r, err)
|
err = db.TransformAndLogDbError("transaction RecalculateBalances", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -413,21 +390,21 @@ func (s TransactionImpl) RecalculateBalances(ctx context.Context, user *types.Us
|
|||||||
}
|
}
|
||||||
|
|
||||||
if transaction.AccountId != nil {
|
if transaction.AccountId != nil {
|
||||||
r, err = tx.ExecContext(ctx, `
|
r, err = tx.Exec(`
|
||||||
UPDATE account
|
UPDATE account
|
||||||
SET current_balance = current_balance + ?
|
SET current_balance = current_balance + ?
|
||||||
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
|
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.AccountId, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", r, err)
|
err = db.TransformAndLogDbError("transaction RecalculateBalances", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if transaction.TreasureChestId != nil {
|
if transaction.TreasureChestId != nil {
|
||||||
r, err = tx.ExecContext(ctx, `
|
r, err = tx.Exec(`
|
||||||
UPDATE treasure_chest
|
UPDATE treasure_chest
|
||||||
SET current_balance = current_balance + ?
|
SET current_balance = current_balance + ?
|
||||||
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
|
WHERE id = ? AND user_id = ?`, transaction.Value, transaction.TreasureChestId, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", r, err)
|
err = db.TransformAndLogDbError("transaction RecalculateBalances", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -435,7 +412,7 @@ func (s TransactionImpl) RecalculateBalances(ctx context.Context, user *types.Us
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction RecalculateBalances", nil, err)
|
err = db.TransformAndLogDbError("transaction RecalculateBalances", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -443,7 +420,7 @@ func (s TransactionImpl) RecalculateBalances(ctx context.Context, user *types.Us
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) validateAndEnrichTransaction(ctx context.Context, tx *sqlx.Tx, oldTransaction *types.Transaction, userId uuid.UUID, input types.Transaction) (*types.Transaction, error) {
|
func (s TransactionImpl) validateAndEnrichTransaction(tx *sqlx.Tx, oldTransaction *types.Transaction, userId uuid.UUID, input types.Transaction) (*types.Transaction, error) {
|
||||||
var (
|
var (
|
||||||
id uuid.UUID
|
id uuid.UUID
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
@@ -456,7 +433,7 @@ func (s TransactionImpl) validateAndEnrichTransaction(ctx context.Context, tx *s
|
|||||||
)
|
)
|
||||||
|
|
||||||
if oldTransaction == nil {
|
if oldTransaction == nil {
|
||||||
id, err = s.random.UUID(ctx)
|
id, err = s.random.UUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
@@ -472,21 +449,21 @@ func (s TransactionImpl) validateAndEnrichTransaction(ctx context.Context, tx *s
|
|||||||
}
|
}
|
||||||
|
|
||||||
if input.AccountId != nil {
|
if input.AccountId != nil {
|
||||||
err = tx.GetContext(ctx, &rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, input.AccountId, userId)
|
err = tx.Get(&rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, input.AccountId, userId)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction validate", nil, err)
|
err = db.TransformAndLogDbError("transaction validate", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if rowCount == 0 {
|
if rowCount == 0 {
|
||||||
slog.ErrorContext(ctx, "transaction validate", "err", err)
|
slog.Error("transaction validate", "err", err)
|
||||||
return nil, fmt.Errorf("account not found: %w", ErrBadRequest)
|
return nil, fmt.Errorf("account not found: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.TreasureChestId != nil {
|
if input.TreasureChestId != nil {
|
||||||
var treasureChest types.TreasureChest
|
var treasureChest types.TreasureChest
|
||||||
err = tx.GetContext(ctx, &treasureChest, `SELECT * FROM treasure_chest WHERE id = ? AND user_id = ?`, input.TreasureChestId, userId)
|
err = tx.Get(&treasureChest, `SELECT * FROM treasure_chest WHERE id = ? AND user_id = ?`, input.TreasureChestId, userId)
|
||||||
err = db.TransformAndLogDbError(ctx, "transaction validate", nil, err)
|
err = db.TransformAndLogDbError("transaction validate", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNotFound) {
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
return nil, fmt.Errorf("treasure chest not found: %w", ErrBadRequest)
|
return nil, fmt.Errorf("treasure chest not found: %w", ErrBadRequest)
|
||||||
@@ -534,20 +511,27 @@ func (s TransactionImpl) validateAndEnrichTransaction(ctx context.Context, tx *s
|
|||||||
return &transaction, nil
|
return &transaction, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionImpl) updateErrors(t *types.Transaction) {
|
func (s TransactionImpl) updateErrors(transaction *types.Transaction) {
|
||||||
errorStr := ""
|
errorStr := ""
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case (t.AccountId != nil && t.TreasureChestId != nil && t.Value > 0) ||
|
case transaction.Value < 0:
|
||||||
(t.AccountId == nil && t.TreasureChestId == nil):
|
if transaction.TreasureChestId == nil {
|
||||||
errorStr = "either an account or a treasure chest needs to be specified"
|
errorStr = "no treasure chest specified"
|
||||||
case t.Value == 0:
|
}
|
||||||
|
case transaction.Value > 0:
|
||||||
|
if transaction.AccountId == nil && transaction.TreasureChestId == nil {
|
||||||
|
errorStr = "either an account or a treasure chest needs to be specified"
|
||||||
|
} else if transaction.AccountId != nil && transaction.TreasureChestId != nil {
|
||||||
|
errorStr = "positive amounts can only be applied to either an account or a treasure chest"
|
||||||
|
}
|
||||||
|
default:
|
||||||
errorStr = "\"value\" needs to be specified"
|
errorStr = "\"value\" needs to be specified"
|
||||||
}
|
}
|
||||||
|
|
||||||
if errorStr == "" {
|
if errorStr == "" {
|
||||||
t.Error = nil
|
transaction.Error = nil
|
||||||
} else {
|
} else {
|
||||||
t.Error = &errorStr
|
transaction.Error = &errorStr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -16,14 +15,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type TransactionRecurring interface {
|
type TransactionRecurring interface {
|
||||||
Add(ctx context.Context, user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error)
|
Add(user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error)
|
||||||
Update(ctx context.Context, user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error)
|
Update(user *types.User, transactionRecurring types.TransactionRecurringInput) (*types.TransactionRecurring, error)
|
||||||
GetAll(ctx context.Context, user *types.User) ([]*types.TransactionRecurring, error)
|
GetAll(user *types.User) ([]*types.TransactionRecurring, error)
|
||||||
GetAllByAccount(ctx context.Context, user *types.User, accountId string) ([]*types.TransactionRecurring, error)
|
GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error)
|
||||||
GetAllByTreasureChest(ctx context.Context, user *types.User, treasureChestId string) ([]*types.TransactionRecurring, error)
|
GetAllByTreasureChest(user *types.User, treasureChestId string) ([]*types.TransactionRecurring, error)
|
||||||
Delete(ctx context.Context, user *types.User, id string) error
|
Delete(user *types.User, id string) error
|
||||||
|
|
||||||
GenerateTransactions(ctx context.Context) error
|
GenerateTransactions(user *types.User) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransactionRecurringImpl struct {
|
type TransactionRecurringImpl struct {
|
||||||
@@ -42,7 +41,7 @@ func NewTransactionRecurring(db *sqlx.DB, random Random, clock Clock, transactio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionRecurringImpl) Add(ctx context.Context,
|
func (s TransactionRecurringImpl) Add(
|
||||||
user *types.User,
|
user *types.User,
|
||||||
transactionRecurringInput types.TransactionRecurringInput,
|
transactionRecurringInput types.TransactionRecurringInput,
|
||||||
) (*types.TransactionRecurring, error) {
|
) (*types.TransactionRecurring, error) {
|
||||||
@@ -50,8 +49,8 @@ func (s TransactionRecurringImpl) Add(ctx context.Context,
|
|||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := s.db.BeginTxx(ctx, nil)
|
tx, err := s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring Add", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring Add", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -59,24 +58,24 @@ func (s TransactionRecurringImpl) Add(ctx context.Context,
|
|||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
transactionRecurring, err := s.validateAndEnrichTransactionRecurring(ctx, tx, nil, user.Id, transactionRecurringInput)
|
transactionRecurring, err := s.validateAndEnrichTransactionRecurring(tx, nil, user.Id, transactionRecurringInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := tx.NamedExecContext(ctx, `
|
r, err := tx.NamedExec(`
|
||||||
INSERT INTO "transaction_recurring" (id, user_id, interval_months,
|
INSERT INTO "transaction_recurring" (id, user_id, interval_months,
|
||||||
next_execution, party, description, account_id, treasure_chest_id, value, created_at, created_by)
|
next_execution, party, description, account_id, treasure_chest_id, value, created_at, created_by)
|
||||||
VALUES (:id, :user_id, :interval_months,
|
VALUES (:id, :user_id, :interval_months,
|
||||||
:next_execution, :party, :description, :account_id, :treasure_chest_id, :value, :created_at, :created_by)`,
|
:next_execution, :party, :description, :account_id, :treasure_chest_id, :value, :created_at, :created_by)`,
|
||||||
transactionRecurring)
|
transactionRecurring)
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring Insert", r, err)
|
err = db.TransformAndLogDbError("transactionRecurring Insert", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring Add", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring Add", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -84,7 +83,7 @@ func (s TransactionRecurringImpl) Add(ctx context.Context,
|
|||||||
return transactionRecurring, nil
|
return transactionRecurring, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionRecurringImpl) Update(ctx context.Context,
|
func (s TransactionRecurringImpl) Update(
|
||||||
user *types.User,
|
user *types.User,
|
||||||
input types.TransactionRecurringInput,
|
input types.TransactionRecurringInput,
|
||||||
) (*types.TransactionRecurring, error) {
|
) (*types.TransactionRecurring, error) {
|
||||||
@@ -93,12 +92,12 @@ func (s TransactionRecurringImpl) Update(ctx context.Context,
|
|||||||
}
|
}
|
||||||
uuid, err := uuid.Parse(input.Id)
|
uuid, err := uuid.Parse(input.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "transactionRecurring update", "err", err)
|
slog.Error("transactionRecurring update", "err", err)
|
||||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := s.db.BeginTxx(ctx, nil)
|
tx, err := s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring Update", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -107,8 +106,8 @@ func (s TransactionRecurringImpl) Update(ctx context.Context,
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
transactionRecurring := &types.TransactionRecurring{}
|
transactionRecurring := &types.TransactionRecurring{}
|
||||||
err = tx.GetContext(ctx, transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
err = tx.Get(transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring Update", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNotFound) {
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
return nil, fmt.Errorf("transactionRecurring %v not found: %w", input.Id, ErrBadRequest)
|
return nil, fmt.Errorf("transactionRecurring %v not found: %w", input.Id, ErrBadRequest)
|
||||||
@@ -116,12 +115,12 @@ func (s TransactionRecurringImpl) Update(ctx context.Context,
|
|||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionRecurring, err = s.validateAndEnrichTransactionRecurring(ctx, tx, transactionRecurring, user.Id, input)
|
transactionRecurring, err = s.validateAndEnrichTransactionRecurring(tx, transactionRecurring, user.Id, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := tx.NamedExecContext(ctx, `
|
r, err := tx.NamedExec(`
|
||||||
UPDATE transaction_recurring
|
UPDATE transaction_recurring
|
||||||
SET
|
SET
|
||||||
interval_months = :interval_months,
|
interval_months = :interval_months,
|
||||||
@@ -135,13 +134,13 @@ func (s TransactionRecurringImpl) Update(ctx context.Context,
|
|||||||
updated_by = :updated_by
|
updated_by = :updated_by
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
AND user_id = :user_id`, transactionRecurring)
|
AND user_id = :user_id`, transactionRecurring)
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring Update", r, err)
|
err = db.TransformAndLogDbError("transactionRecurring Update", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring Update", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -149,19 +148,19 @@ func (s TransactionRecurringImpl) Update(ctx context.Context,
|
|||||||
return transactionRecurring, nil
|
return transactionRecurring, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionRecurringImpl) GetAll(ctx context.Context, user *types.User) ([]*types.TransactionRecurring, error) {
|
func (s TransactionRecurringImpl) GetAll(user *types.User) ([]*types.TransactionRecurring, error) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionRecurrings := make([]*types.TransactionRecurring, 0)
|
transactionRecurrings := make([]*types.TransactionRecurring, 0)
|
||||||
err := s.db.SelectContext(ctx, &transactionRecurrings, `
|
err := s.db.Select(&transactionRecurrings, `
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM transaction_recurring
|
FROM transaction_recurring
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
ORDER BY created_at DESC`,
|
ORDER BY created_at DESC`,
|
||||||
user.Id)
|
user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAll", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring GetAll", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -169,19 +168,19 @@ func (s TransactionRecurringImpl) GetAll(ctx context.Context, user *types.User)
|
|||||||
return transactionRecurrings, nil
|
return transactionRecurrings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionRecurringImpl) GetAllByAccount(ctx context.Context, user *types.User, accountId string) ([]*types.TransactionRecurring, error) {
|
func (s TransactionRecurringImpl) GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
accountUuid, err := uuid.Parse(accountId)
|
accountUuid, err := uuid.Parse(accountId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "transactionRecurring GetAllByAccount", "err", err)
|
slog.Error("transactionRecurring GetAllByAccount", "err", err)
|
||||||
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := s.db.BeginTxx(ctx, nil)
|
tx, err := s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAllByAccount", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring GetAllByAccount", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -190,8 +189,8 @@ func (s TransactionRecurringImpl) GetAllByAccount(ctx context.Context, user *typ
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
var rowCount int
|
var rowCount int
|
||||||
err = tx.GetContext(ctx, &rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, user.Id)
|
err = tx.Get(&rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAllByAccount", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring GetAllByAccount", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNotFound) {
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
return nil, fmt.Errorf("account %v not found: %w", accountId, ErrBadRequest)
|
return nil, fmt.Errorf("account %v not found: %w", accountId, ErrBadRequest)
|
||||||
@@ -200,20 +199,20 @@ func (s TransactionRecurringImpl) GetAllByAccount(ctx context.Context, user *typ
|
|||||||
}
|
}
|
||||||
|
|
||||||
transactionRecurrings := make([]*types.TransactionRecurring, 0)
|
transactionRecurrings := make([]*types.TransactionRecurring, 0)
|
||||||
err = tx.SelectContext(ctx, &transactionRecurrings, `
|
err = tx.Select(&transactionRecurrings, `
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM transaction_recurring
|
FROM transaction_recurring
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
AND account_id = ?
|
AND account_id = ?
|
||||||
ORDER BY created_at DESC`,
|
ORDER BY created_at DESC`,
|
||||||
user.Id, accountUuid)
|
user.Id, accountUuid)
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAll", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring GetAll", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAllByAccount", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring GetAllByAccount", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -221,7 +220,7 @@ func (s TransactionRecurringImpl) GetAllByAccount(ctx context.Context, user *typ
|
|||||||
return transactionRecurrings, nil
|
return transactionRecurrings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionRecurringImpl) GetAllByTreasureChest(ctx context.Context,
|
func (s TransactionRecurringImpl) GetAllByTreasureChest(
|
||||||
user *types.User,
|
user *types.User,
|
||||||
treasureChestId string,
|
treasureChestId string,
|
||||||
) ([]*types.TransactionRecurring, error) {
|
) ([]*types.TransactionRecurring, error) {
|
||||||
@@ -231,12 +230,12 @@ func (s TransactionRecurringImpl) GetAllByTreasureChest(ctx context.Context,
|
|||||||
|
|
||||||
treasureChestUuid, err := uuid.Parse(treasureChestId)
|
treasureChestUuid, err := uuid.Parse(treasureChestId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "transactionRecurring GetAllByTreasureChest", "err", err)
|
slog.Error("transactionRecurring GetAllByTreasureChest", "err", err)
|
||||||
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := s.db.BeginTxx(ctx, nil)
|
tx, err := s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAllByTreasureChest", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring GetAllByTreasureChest", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -245,8 +244,8 @@ func (s TransactionRecurringImpl) GetAllByTreasureChest(ctx context.Context,
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
var rowCount int
|
var rowCount int
|
||||||
err = tx.GetContext(ctx, &rowCount, `SELECT COUNT(*) FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestId, user.Id)
|
err = tx.Get(&rowCount, `SELECT COUNT(*) FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestId, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAllByTreasureChest", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring GetAllByTreasureChest", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNotFound) {
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
return nil, fmt.Errorf("treasurechest %v not found: %w", treasureChestId, ErrBadRequest)
|
return nil, fmt.Errorf("treasurechest %v not found: %w", treasureChestId, ErrBadRequest)
|
||||||
@@ -255,20 +254,20 @@ func (s TransactionRecurringImpl) GetAllByTreasureChest(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
transactionRecurrings := make([]*types.TransactionRecurring, 0)
|
transactionRecurrings := make([]*types.TransactionRecurring, 0)
|
||||||
err = tx.SelectContext(ctx, &transactionRecurrings, `
|
err = tx.Select(&transactionRecurrings, `
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM transaction_recurring
|
FROM transaction_recurring
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
AND treasure_chest_id = ?
|
AND treasure_chest_id = ?
|
||||||
ORDER BY created_at DESC`,
|
ORDER BY created_at DESC`,
|
||||||
user.Id, treasureChestUuid)
|
user.Id, treasureChestUuid)
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAll", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring GetAll", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring GetAllByTreasureChest", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring GetAllByTreasureChest", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -276,18 +275,18 @@ func (s TransactionRecurringImpl) GetAllByTreasureChest(ctx context.Context,
|
|||||||
return transactionRecurrings, nil
|
return transactionRecurrings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionRecurringImpl) Delete(ctx context.Context, user *types.User, id string) error {
|
func (s TransactionRecurringImpl) Delete(user *types.User, id string) error {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return ErrUnauthorized
|
return ErrUnauthorized
|
||||||
}
|
}
|
||||||
uuid, err := uuid.Parse(id)
|
uuid, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "transactionRecurring delete", "err", err)
|
slog.Error("transactionRecurring delete", "err", err)
|
||||||
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := s.db.BeginTxx(ctx, nil)
|
tx, err := s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring Delete", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -296,20 +295,20 @@ func (s TransactionRecurringImpl) Delete(ctx context.Context, user *types.User,
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
var transactionRecurring types.TransactionRecurring
|
var transactionRecurring types.TransactionRecurring
|
||||||
err = tx.GetContext(ctx, &transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
err = tx.Get(&transactionRecurring, `SELECT * FROM transaction_recurring WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring Delete", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := tx.ExecContext(ctx, "DELETE FROM transaction_recurring WHERE id = ? AND user_id = ?", uuid, user.Id)
|
r, err := tx.Exec("DELETE FROM transaction_recurring WHERE id = ? AND user_id = ?", uuid, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring Delete", r, err)
|
err = db.TransformAndLogDbError("transactionRecurring Delete", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring Delete", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -317,11 +316,14 @@ func (s TransactionRecurringImpl) Delete(ctx context.Context, user *types.User,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionRecurringImpl) GenerateTransactions(ctx context.Context) error {
|
func (s TransactionRecurringImpl) GenerateTransactions(user *types.User) error {
|
||||||
|
if user == nil {
|
||||||
|
return ErrUnauthorized
|
||||||
|
}
|
||||||
now := s.clock.Now()
|
now := s.clock.Now()
|
||||||
|
|
||||||
tx, err := s.db.BeginTxx(ctx, nil)
|
tx, err := s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring GenerateTransactions", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -330,18 +332,15 @@ func (s TransactionRecurringImpl) GenerateTransactions(ctx context.Context) erro
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
recurringTransactions := make([]*types.TransactionRecurring, 0)
|
recurringTransactions := make([]*types.TransactionRecurring, 0)
|
||||||
err = tx.SelectContext(ctx, &recurringTransactions, `
|
err = tx.Select(&recurringTransactions, `
|
||||||
SELECT * FROM transaction_recurring WHERE next_execution <= ?`,
|
SELECT * FROM transaction_recurring WHERE user_id = ? AND next_execution <= ?`,
|
||||||
now)
|
user.Id, now)
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring GenerateTransactions", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, transactionRecurring := range recurringTransactions {
|
for _, transactionRecurring := range recurringTransactions {
|
||||||
user := &types.User{
|
|
||||||
Id: transactionRecurring.UserId,
|
|
||||||
}
|
|
||||||
transaction := types.Transaction{
|
transaction := types.Transaction{
|
||||||
Timestamp: *transactionRecurring.NextExecution,
|
Timestamp: *transactionRecurring.NextExecution,
|
||||||
Party: transactionRecurring.Party,
|
Party: transactionRecurring.Party,
|
||||||
@@ -351,22 +350,22 @@ func (s TransactionRecurringImpl) GenerateTransactions(ctx context.Context) erro
|
|||||||
Value: transactionRecurring.Value,
|
Value: transactionRecurring.Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = s.transaction.Add(ctx, tx, user, transaction)
|
_, err = s.transaction.Add(tx, user, transaction)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
nextExecution := transactionRecurring.NextExecution.AddDate(0, int(transactionRecurring.IntervalMonths), 0)
|
nextExecution := transactionRecurring.NextExecution.AddDate(0, int(transactionRecurring.IntervalMonths), 0)
|
||||||
r, err := tx.ExecContext(ctx, `UPDATE transaction_recurring SET next_execution = ? WHERE id = ? AND user_id = ?`,
|
r, err := tx.Exec(`UPDATE transaction_recurring SET next_execution = ? WHERE id = ? AND user_id = ?`,
|
||||||
nextExecution, transactionRecurring.Id, user.Id)
|
nextExecution, transactionRecurring.Id, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", r, err)
|
err = db.TransformAndLogDbError("transactionRecurring GenerateTransactions", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring GenerateTransactions", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring GenerateTransactions", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -374,7 +373,6 @@ func (s TransactionRecurringImpl) GenerateTransactions(ctx context.Context) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
|
func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
|
||||||
ctx context.Context,
|
|
||||||
tx *sqlx.Tx,
|
tx *sqlx.Tx,
|
||||||
oldTransactionRecurring *types.TransactionRecurring,
|
oldTransactionRecurring *types.TransactionRecurring,
|
||||||
userId uuid.UUID,
|
userId uuid.UUID,
|
||||||
@@ -395,7 +393,7 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if oldTransactionRecurring == nil {
|
if oldTransactionRecurring == nil {
|
||||||
id, err = s.random.UUID(ctx)
|
id, err = s.random.UUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
@@ -415,17 +413,17 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
|
|||||||
if input.AccountId != "" {
|
if input.AccountId != "" {
|
||||||
temp, err := uuid.Parse(input.AccountId)
|
temp, err := uuid.Parse(input.AccountId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
slog.Error("transactionRecurring validate", "err", err)
|
||||||
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
accountUuid = &temp
|
accountUuid = &temp
|
||||||
err = tx.GetContext(ctx, &rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, userId)
|
err = tx.Get(&rowCount, `SELECT COUNT(*) FROM account WHERE id = ? AND user_id = ?`, accountUuid, userId)
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring validate", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring validate", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if rowCount == 0 {
|
if rowCount == 0 {
|
||||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
slog.Error("transactionRecurring validate", "err", err)
|
||||||
return nil, fmt.Errorf("account not found: %w", ErrBadRequest)
|
return nil, fmt.Errorf("account not found: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,13 +433,13 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
|
|||||||
if input.TreasureChestId != "" {
|
if input.TreasureChestId != "" {
|
||||||
temp, err := uuid.Parse(input.TreasureChestId)
|
temp, err := uuid.Parse(input.TreasureChestId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
slog.Error("transactionRecurring validate", "err", err)
|
||||||
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
treasureChestUuid = &temp
|
treasureChestUuid = &temp
|
||||||
var treasureChest types.TreasureChest
|
var treasureChest types.TreasureChest
|
||||||
err = tx.GetContext(ctx, &treasureChest, `SELECT * FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestUuid, userId)
|
err = tx.Get(&treasureChest, `SELECT * FROM treasure_chest WHERE id = ? AND user_id = ?`, treasureChestUuid, userId)
|
||||||
err = db.TransformAndLogDbError(ctx, "transactionRecurring validate", nil, err)
|
err = db.TransformAndLogDbError("transactionRecurring validate", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNotFound) {
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
return nil, fmt.Errorf("treasure chest not found: %w", ErrBadRequest)
|
return nil, fmt.Errorf("treasure chest not found: %w", ErrBadRequest)
|
||||||
@@ -455,17 +453,17 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !hasAccount && !hasTreasureChest {
|
if !hasAccount && !hasTreasureChest {
|
||||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
slog.Error("transactionRecurring validate", "err", err)
|
||||||
return nil, fmt.Errorf("either account or treasure chest is required: %w", ErrBadRequest)
|
return nil, fmt.Errorf("either account or treasure chest is required: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
if hasAccount && hasTreasureChest {
|
if hasAccount && hasTreasureChest {
|
||||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
slog.Error("transactionRecurring validate", "err", err)
|
||||||
return nil, fmt.Errorf("either account or treasure chest is required, not both: %w", ErrBadRequest)
|
return nil, fmt.Errorf("either account or treasure chest is required, not both: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
valueFloat, err := strconv.ParseFloat(input.Value, 64)
|
valueFloat, err := strconv.ParseFloat(input.Value, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
slog.Error("transactionRecurring validate", "err", err)
|
||||||
return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
value := int64(math.Round(valueFloat * DECIMALS_MULTIPLIER))
|
value := int64(math.Round(valueFloat * DECIMALS_MULTIPLIER))
|
||||||
@@ -484,18 +482,18 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
|
|||||||
}
|
}
|
||||||
intervalMonths, err = strconv.ParseInt(input.IntervalMonths, 10, 0)
|
intervalMonths, err = strconv.ParseInt(input.IntervalMonths, 10, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
slog.Error("transactionRecurring validate", "err", err)
|
||||||
return nil, fmt.Errorf("could not parse intervalMonths: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse intervalMonths: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
if intervalMonths < 1 {
|
if intervalMonths < 1 {
|
||||||
slog.ErrorContext(ctx, "transactionRecurring validate", "err", err)
|
slog.Error("transactionRecurring validate", "err", err)
|
||||||
return nil, fmt.Errorf("intervalMonths needs to be greater than 0: %w", ErrBadRequest)
|
return nil, fmt.Errorf("intervalMonths needs to be greater than 0: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
var nextExecution *time.Time = nil
|
var nextExecution *time.Time = nil
|
||||||
if input.NextExecution != "" {
|
if input.NextExecution != "" {
|
||||||
t, err := time.Parse("2006-01-02", input.NextExecution)
|
t, err := time.Parse("2006-01-02", input.NextExecution)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "transaction validate", "err", err)
|
slog.Error("transaction validate", "err", err)
|
||||||
return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -14,11 +13,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type TreasureChest interface {
|
type TreasureChest interface {
|
||||||
Add(ctx context.Context, user *types.User, parentId, name string) (*types.TreasureChest, error)
|
Add(user *types.User, parentId, name string) (*types.TreasureChest, error)
|
||||||
Update(ctx context.Context, user *types.User, id, parentId, name string) (*types.TreasureChest, error)
|
Update(user *types.User, id, parentId, name string) (*types.TreasureChest, error)
|
||||||
Get(ctx context.Context, user *types.User, id string) (*types.TreasureChest, error)
|
Get(user *types.User, id string) (*types.TreasureChest, error)
|
||||||
GetAll(ctx context.Context, user *types.User) ([]*types.TreasureChest, error)
|
GetAll(user *types.User) ([]*types.TreasureChest, error)
|
||||||
Delete(ctx context.Context, user *types.User, id string) error
|
Delete(user *types.User, id string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type TreasureChestImpl struct {
|
type TreasureChestImpl struct {
|
||||||
@@ -35,12 +34,12 @@ func NewTreasureChest(db *sqlx.DB, random Random, clock Clock) TreasureChest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TreasureChestImpl) Add(ctx context.Context, user *types.User, parentId, name string) (*types.TreasureChest, error) {
|
func (s TreasureChestImpl) Add(user *types.User, parentId, name string) (*types.TreasureChest, error) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
newId, err := s.random.UUID(ctx)
|
newId, err := s.random.UUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.ErrInternal
|
return nil, types.ErrInternal
|
||||||
}
|
}
|
||||||
@@ -52,7 +51,7 @@ func (s TreasureChestImpl) Add(ctx context.Context, user *types.User, parentId,
|
|||||||
|
|
||||||
var parentUuid *uuid.UUID
|
var parentUuid *uuid.UUID
|
||||||
if parentId != "" {
|
if parentId != "" {
|
||||||
parent, err := s.Get(ctx, user, parentId)
|
parent, err := s.Get(user, parentId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -77,10 +76,10 @@ func (s TreasureChestImpl) Add(ctx context.Context, user *types.User, parentId,
|
|||||||
UpdatedBy: nil,
|
UpdatedBy: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := s.db.NamedExecContext(ctx, `
|
r, err := s.db.NamedExec(`
|
||||||
INSERT INTO treasure_chest (id, parent_id, user_id, name, current_balance, created_at, created_by)
|
INSERT INTO treasure_chest (id, parent_id, user_id, name, current_balance, created_at, created_by)
|
||||||
VALUES (:id, :parent_id, :user_id, :name, :current_balance, :created_at, :created_by)`, treasureChest)
|
VALUES (:id, :parent_id, :user_id, :name, :current_balance, :created_at, :created_by)`, treasureChest)
|
||||||
err = db.TransformAndLogDbError(ctx, "treasureChest Insert", r, err)
|
err = db.TransformAndLogDbError("treasureChest Insert", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -88,7 +87,7 @@ func (s TreasureChestImpl) Add(ctx context.Context, user *types.User, parentId,
|
|||||||
return treasureChest, nil
|
return treasureChest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TreasureChestImpl) Update(ctx context.Context, user *types.User, idStr, parentId, name string) (*types.TreasureChest, error) {
|
func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string) (*types.TreasureChest, error) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
@@ -98,12 +97,12 @@ func (s TreasureChestImpl) Update(ctx context.Context, user *types.User, idStr,
|
|||||||
}
|
}
|
||||||
id, err := uuid.Parse(idStr)
|
id, err := uuid.Parse(idStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "treasureChest update", "err", err)
|
slog.Error("treasureChest update", "err", err)
|
||||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := s.db.BeginTxx(ctx, nil)
|
tx, err := s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError(ctx, "treasureChest Update", nil, err)
|
err = db.TransformAndLogDbError("treasureChest Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -112,8 +111,8 @@ func (s TreasureChestImpl) Update(ctx context.Context, user *types.User, idStr,
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
treasureChest := &types.TreasureChest{}
|
treasureChest := &types.TreasureChest{}
|
||||||
err = tx.GetContext(ctx, treasureChest, `SELECT * FROM treasure_chest WHERE user_id = ? AND id = ?`, user.Id, id)
|
err = tx.Get(treasureChest, `SELECT * FROM treasure_chest WHERE user_id = ? AND id = ?`, user.Id, id)
|
||||||
err = db.TransformAndLogDbError(ctx, "treasureChest Update", nil, err)
|
err = db.TransformAndLogDbError("treasureChest Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNotFound) {
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
return nil, fmt.Errorf("treasureChest %v not found: %w", idStr, err)
|
return nil, fmt.Errorf("treasureChest %v not found: %w", idStr, err)
|
||||||
@@ -123,13 +122,13 @@ func (s TreasureChestImpl) Update(ctx context.Context, user *types.User, idStr,
|
|||||||
|
|
||||||
var parentUuid *uuid.UUID
|
var parentUuid *uuid.UUID
|
||||||
if parentId != "" {
|
if parentId != "" {
|
||||||
parent, err := s.Get(ctx, user, parentId)
|
parent, err := s.Get(user, parentId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var childCount int
|
var childCount int
|
||||||
err = tx.GetContext(ctx, &childCount, `SELECT COUNT(*) FROM treasure_chest WHERE user_id = ? AND parent_id = ?`, user.Id, id)
|
err = tx.Get(&childCount, `SELECT COUNT(*) FROM treasure_chest WHERE user_id = ? AND parent_id = ?`, user.Id, id)
|
||||||
err = db.TransformAndLogDbError(ctx, "treasureChest Update", nil, err)
|
err = db.TransformAndLogDbError("treasureChest Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -146,7 +145,7 @@ func (s TreasureChestImpl) Update(ctx context.Context, user *types.User, idStr,
|
|||||||
treasureChest.UpdatedAt = ×tamp
|
treasureChest.UpdatedAt = ×tamp
|
||||||
treasureChest.UpdatedBy = &user.Id
|
treasureChest.UpdatedBy = &user.Id
|
||||||
|
|
||||||
r, err := tx.NamedExecContext(ctx, `
|
r, err := tx.NamedExec(`
|
||||||
UPDATE treasure_chest
|
UPDATE treasure_chest
|
||||||
SET
|
SET
|
||||||
parent_id = :parent_id,
|
parent_id = :parent_id,
|
||||||
@@ -156,13 +155,13 @@ func (s TreasureChestImpl) Update(ctx context.Context, user *types.User, idStr,
|
|||||||
updated_by = :updated_by
|
updated_by = :updated_by
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
AND user_id = :user_id`, treasureChest)
|
AND user_id = :user_id`, treasureChest)
|
||||||
err = db.TransformAndLogDbError(ctx, "treasureChest Update", r, err)
|
err = db.TransformAndLogDbError("treasureChest Update", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
err = db.TransformAndLogDbError(ctx, "treasureChest Update", nil, err)
|
err = db.TransformAndLogDbError("treasureChest Update", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -170,19 +169,19 @@ func (s TreasureChestImpl) Update(ctx context.Context, user *types.User, idStr,
|
|||||||
return treasureChest, nil
|
return treasureChest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TreasureChestImpl) Get(ctx context.Context, user *types.User, id string) (*types.TreasureChest, error) {
|
func (s TreasureChestImpl) Get(user *types.User, id string) (*types.TreasureChest, error) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
uuid, err := uuid.Parse(id)
|
uuid, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "treasureChest get", "err", err)
|
slog.Error("treasureChest get", "err", err)
|
||||||
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
var treasureChest types.TreasureChest
|
var treasureChest types.TreasureChest
|
||||||
err = s.db.GetContext(ctx, &treasureChest, `SELECT * FROM treasure_chest WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
err = s.db.Get(&treasureChest, `SELECT * FROM treasure_chest WHERE user_id = ? AND id = ?`, user.Id, uuid)
|
||||||
err = db.TransformAndLogDbError(ctx, "treasureChest Get", nil, err)
|
err = db.TransformAndLogDbError("treasureChest Get", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNotFound) {
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
return nil, fmt.Errorf("treasureChest %v not found: %w", id, err)
|
return nil, fmt.Errorf("treasureChest %v not found: %w", id, err)
|
||||||
@@ -193,33 +192,33 @@ func (s TreasureChestImpl) Get(ctx context.Context, user *types.User, id string)
|
|||||||
return &treasureChest, nil
|
return &treasureChest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TreasureChestImpl) GetAll(ctx context.Context, user *types.User) ([]*types.TreasureChest, error) {
|
func (s TreasureChestImpl) GetAll(user *types.User) ([]*types.TreasureChest, error) {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
treasureChests := make([]*types.TreasureChest, 0)
|
treasureChests := make([]*types.TreasureChest, 0)
|
||||||
err := s.db.SelectContext(ctx, &treasureChests, `SELECT * FROM treasure_chest WHERE user_id = ?`, user.Id)
|
err := s.db.Select(&treasureChests, `SELECT * FROM treasure_chest WHERE user_id = ?`, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "treasureChest GetAll", nil, err)
|
err = db.TransformAndLogDbError("treasureChest GetAll", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortTreasureChests(treasureChests), nil
|
return sortTree(treasureChests), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s TreasureChestImpl) Delete(ctx context.Context, user *types.User, idStr string) error {
|
func (s TreasureChestImpl) Delete(user *types.User, idStr string) error {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return ErrUnauthorized
|
return ErrUnauthorized
|
||||||
}
|
}
|
||||||
id, err := uuid.Parse(idStr)
|
id, err := uuid.Parse(idStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "treasureChest delete", "err", err)
|
slog.Error("treasureChest delete", "err", err)
|
||||||
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := s.db.BeginTxx(ctx, nil)
|
tx, err := s.db.Beginx()
|
||||||
err = db.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
|
err = db.TransformAndLogDbError("treasureChest Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -228,8 +227,8 @@ func (s TreasureChestImpl) Delete(ctx context.Context, user *types.User, idStr s
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
childCount := 0
|
childCount := 0
|
||||||
err = tx.GetContext(ctx, &childCount, `SELECT COUNT(*) FROM treasure_chest WHERE user_id = ? AND parent_id = ?`, user.Id, id)
|
err = tx.Get(&childCount, `SELECT COUNT(*) FROM treasure_chest WHERE user_id = ? AND parent_id = ?`, user.Id, id)
|
||||||
err = db.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
|
err = db.TransformAndLogDbError("treasureChest Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -239,10 +238,10 @@ func (s TreasureChestImpl) Delete(ctx context.Context, user *types.User, idStr s
|
|||||||
}
|
}
|
||||||
|
|
||||||
transactionsCount := 0
|
transactionsCount := 0
|
||||||
err = tx.GetContext(ctx, &transactionsCount,
|
err = tx.Get(&transactionsCount,
|
||||||
`SELECT COUNT(*) FROM "transaction" WHERE user_id = ? AND treasure_chest_id = ?`,
|
`SELECT COUNT(*) FROM "transaction" WHERE user_id = ? AND treasure_chest_id = ?`,
|
||||||
user.Id, id)
|
user.Id, id)
|
||||||
err = db.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
|
err = db.TransformAndLogDbError("treasureChest Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -251,10 +250,10 @@ func (s TreasureChestImpl) Delete(ctx context.Context, user *types.User, idStr s
|
|||||||
}
|
}
|
||||||
|
|
||||||
recurringCount := 0
|
recurringCount := 0
|
||||||
err = tx.GetContext(ctx, &recurringCount, `
|
err = tx.Get(&recurringCount, `
|
||||||
SELECT COUNT(*) FROM transaction_recurring WHERE user_id = ? AND treasure_chest_id = ?`,
|
SELECT COUNT(*) FROM transaction_recurring WHERE user_id = ? AND treasure_chest_id = ?`,
|
||||||
user.Id, id)
|
user.Id, id)
|
||||||
err = db.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
|
err = db.TransformAndLogDbError("treasureChest Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -262,14 +261,14 @@ func (s TreasureChestImpl) Delete(ctx context.Context, user *types.User, idStr s
|
|||||||
return fmt.Errorf("cannot delete treasure chest with existing recurring transactions: %w", ErrBadRequest)
|
return fmt.Errorf("cannot delete treasure chest with existing recurring transactions: %w", ErrBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := tx.ExecContext(ctx, `DELETE FROM treasure_chest WHERE id = ? AND user_id = ?`, id, user.Id)
|
r, err := tx.Exec(`DELETE FROM treasure_chest WHERE id = ? AND user_id = ?`, id, user.Id)
|
||||||
err = db.TransformAndLogDbError(ctx, "treasureChest Delete", r, err)
|
err = db.TransformAndLogDbError("treasureChest Delete", r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
err = db.TransformAndLogDbError(ctx, "treasureChest Delete", nil, err)
|
err = db.TransformAndLogDbError("treasureChest Delete", nil, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -277,7 +276,7 @@ func (s TreasureChestImpl) Delete(ctx context.Context, user *types.User, idStr s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortTreasureChests(nodes []*types.TreasureChest) []*types.TreasureChest {
|
func sortTree(nodes []*types.TreasureChest) []*types.TreasureChest {
|
||||||
var (
|
var (
|
||||||
roots []*types.TreasureChest
|
roots []*types.TreasureChest
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package account
|
package account
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
import "spend-sparrow/internal/template/svg"
|
import "spend-sparrow/internal/template/svg"
|
||||||
import "spend-sparrow/internal/types"
|
import "spend-sparrow/internal/types"
|
||||||
|
|
||||||
@@ -66,9 +67,7 @@ templ EditAccount(account *types.Account) {
|
|||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
class="button button-neglect px-1 flex items-center gap-2"
|
class="button button-neglect px-1 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<span class="h-4 w-4">
|
@svg.Cancel()
|
||||||
@svg.Cancel()
|
|
||||||
</span>
|
|
||||||
<span>
|
<span>
|
||||||
Cancel
|
Cancel
|
||||||
</span>
|
</span>
|
||||||
@@ -82,9 +81,9 @@ templ AccountItem(account *types.Account) {
|
|||||||
<div class="text-xl flex justify-end gap-4">
|
<div class="text-xl flex justify-end gap-4">
|
||||||
<p class="mr-auto">{ account.Name }</p>
|
<p class="mr-auto">{ account.Name }</p>
|
||||||
if account.CurrentBalance < 0 {
|
if account.CurrentBalance < 0 {
|
||||||
<p class="mr-20 text-red-700">{ types.FormatEuros(account.CurrentBalance) }</p>
|
<p class="mr-20 text-red-700">{ displayBalance(account.CurrentBalance) }</p>
|
||||||
} else {
|
} else {
|
||||||
<p class="mr-20 text-green-700">{ types.FormatEuros(account.CurrentBalance) }</p>
|
<p class="mr-20 text-green-700">{ displayBalance(account.CurrentBalance) }</p>
|
||||||
}
|
}
|
||||||
<a
|
<a
|
||||||
href={ templ.URL("/transaction?account-id=" + account.Id.String()) }
|
href={ templ.URL("/transaction?account-id=" + account.Id.String()) }
|
||||||
@@ -122,3 +121,9 @@ templ AccountItem(account *types.Account) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func displayBalance(balance int64) string {
|
||||||
|
|
||||||
|
euros := float64(balance) / 100
|
||||||
|
return fmt.Sprintf("%.2f €", euros)
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,10 +71,10 @@ if isSignIn {
|
|||||||
Don't have an account?
|
Don't have an account?
|
||||||
Sign Up
|
Sign Up
|
||||||
</a>
|
</a>
|
||||||
<button class="button button-primary text-gray-600 text-2xl px-1">Sign In</button>
|
<button class="button button-primary font-pirata text-gray-600 text-2xl px-1">Sign In</button>
|
||||||
} else {
|
} else {
|
||||||
<a href="/auth/signin" class="text-gray-500 text-sm px-1 button button-neglect">Already have an account? Sign In</a>
|
<a href="/auth/signin" class="text-gray-500 text-sm px-1 button button-neglect">Already have an account? Sign In</a>
|
||||||
<button class="button button-primary text-gray-600 text-2xl px-1">
|
<button class="button button-primary font-pirata text-gray-600 text-2xl px-1">
|
||||||
Sign Up
|
Sign Up
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
templ UserComp(user string) {
|
templ UserComp(user string) {
|
||||||
<div id="user-info" class="flex items-center gap-2 text-nowrap">
|
<div id="user-info" class="flex gap-5 items-center">
|
||||||
if user != "" {
|
if user != "" {
|
||||||
<div class="inline-block group relative">
|
<div class="inline-block group relative">
|
||||||
<button class="font-semibold py-2 px-4 inline-flex items-center">
|
<button class="font-semibold py-2 px-4 inline-flex items-center">
|
||||||
@@ -37,8 +37,8 @@ templ UserComp(user string) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
<a href="/auth/signup" class="text-xl button px-1 button-neglect">Sign Up</a>
|
<a href="/auth/signup" class="font-pirata text-xl button px-1 button-neglect">Sign Up</a>
|
||||||
<a href="/auth/signin" class="text-xl button px-1 button-neglect">Sign In</a>
|
<a href="/auth/signin" class="font-pirata text-xl button px-1 button-neglect">Sign In</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
9
internal/template/dashboard.templ
Normal file
9
internal/template/dashboard.templ
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
templ Dashboard() {
|
||||||
|
<div>
|
||||||
|
<h1 class="text-8xl">
|
||||||
|
Dashboard
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package dashboard
|
|
||||||
|
|
||||||
import "spend-sparrow/internal/types"
|
|
||||||
|
|
||||||
templ Dashboard(treasureChests []*types.TreasureChest) {
|
|
||||||
<div class="mt-10 h-full">
|
|
||||||
<div id="main-chart" class="h-96 mt-10"></div>
|
|
||||||
<div id="treasure-chests" class="h-96 mt-10"></div>
|
|
||||||
<section>
|
|
||||||
<form class="flex items-center justify-end gap-4 mr-40">
|
|
||||||
<label for="treasure-chest">Treasure Chest:</label>
|
|
||||||
<select id="treasure-chest-id" name="treasure-chest-id" class="bg-white input">
|
|
||||||
<option value="">- Select Treasure Chest -</option>
|
|
||||||
for _, parent := range treasureChests {
|
|
||||||
if parent.ParentId == nil {
|
|
||||||
<optgroup label={ parent.Name }>
|
|
||||||
for _, child := range treasureChests {
|
|
||||||
if child.ParentId != nil && *child.ParentId == parent.Id {
|
|
||||||
<option
|
|
||||||
value={ child.Id.String() }
|
|
||||||
>{ child.Name }</option>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</optgroup>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
<div id="treasure-chest" class="h-96 mt-10"></div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
package dashboard
|
|
||||||
|
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
package template
|
package template
|
||||||
|
|
||||||
import "spend-sparrow/internal/template/svg"
|
|
||||||
|
|
||||||
func layoutLinkClass(isActive bool) string {
|
func layoutLinkClass(isActive bool) string {
|
||||||
common := "text-2xl p-2 text-gray-900 decoration-yellow-400 decoration-[0.25rem] hover:bg-gray-200 rounded-lg"
|
|
||||||
if isActive {
|
if isActive {
|
||||||
return common + " " + "underline"
|
return "text-xl hover:bg-gray-100 p-1 duration-100 rounded-xl transition-colors text-gray-900"
|
||||||
}
|
}
|
||||||
|
return "text-xl hover:bg-gray-100 hover:text-gray-900 p-1 duration-200 rounded-xl transition-colors text-gray-400"
|
||||||
return common + " " + "hover:underline"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Layout(slot templ.Component, user templ.Component, loggedIn bool, path string) {
|
templ Layout(slot templ.Component, user templ.Component, loggedIn bool, path string) {
|
||||||
@@ -26,70 +22,46 @@ templ Layout(slot templ.Component, user templ.Component, loggedIn bool, path str
|
|||||||
"includeIndicatorStyles": false,
|
"includeIndicatorStyles": false,
|
||||||
"selfRequestsOnly": true,
|
"selfRequestsOnly": true,
|
||||||
"allowScriptTags": false
|
"allowScriptTags": false
|
||||||
}'
|
}'
|
||||||
/>
|
/>
|
||||||
<script src="/static/js/htmx.min.js"></script>
|
<script src="/static/js/htmx.min.js"></script>
|
||||||
<script src="/static/js/toast.js"></script>
|
<script src="/static/js/toast.js"></script>
|
||||||
<script src="/static/js/layout.js"></script>
|
|
||||||
<script src="/static/js/transaction.js"></script>
|
|
||||||
<script src="/static/js/time.js"></script>
|
<script src="/static/js/time.js"></script>
|
||||||
<script src="/static/js/echarts.min.js"></script>
|
|
||||||
<script src="/static/js/dashboard.js" defer></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body hx-headers='{"Csrf-Token": "CSRF_TOKEN"}'>
|
<body class="h-screen flex flex-col" hx-headers='{"Csrf-Token": "CSRF_TOKEN"}'>
|
||||||
<div class="flex flex-col min-h-screen">
|
// Header
|
||||||
<header class="sticky top-0 z-50 bg-white flex items-center gap-6 p-4 border-b-1 border-gray-200">
|
<nav class="flex bg-white items-center gap-2 py-1 px-2 h-12 md:gap-10 md:px-10 md:py-2">
|
||||||
<button id="menuButton" class="w-10 h-10 block xl:hidden">
|
<a href="/" class="flex gap-2 mr-20">
|
||||||
@svg.Menu()
|
<img class="w-6" src="/static/favicon.svg" alt="SpendSparrow logo"/>
|
||||||
</button>
|
<span class="text-4xl font-bold font-pirata">SpendSparrow</span>
|
||||||
<a href="/" class="flex gap-2 -mt-2">
|
</a>
|
||||||
<img width="150" src="/static/logo.svg" alt="SpendSparrow logo"/>
|
if loggedIn {
|
||||||
</a>
|
<a class={ layoutLinkClass(path == "/") } href="/">Dashboard</a>
|
||||||
<div class="ml-auto">
|
<a class={ layoutLinkClass(path == "/transaction") } href="/transaction">Transaction</a>
|
||||||
@user
|
<a class={ layoutLinkClass(path == "/treasurechest") } href="/treasurechest">Treasure Chest</a>
|
||||||
</div>
|
<a class={ layoutLinkClass(path == "/account") } href="/account">Account</a>
|
||||||
</header>
|
}
|
||||||
// Content
|
<div class="ml-auto">
|
||||||
<div class="flex flex-1">
|
@user
|
||||||
if loggedIn {
|
|
||||||
<aside class="shrink-0 h-[calc(100vh-4rem)] xl:block hidden sticky top-18 border-r-1 border-gray-200 overflow-y-auto p-4">
|
|
||||||
@navigation(path)
|
|
||||||
</aside>
|
|
||||||
}
|
|
||||||
<main class="flex-1 p-6">
|
|
||||||
if slot != nil {
|
|
||||||
@slot
|
|
||||||
}
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
<dialog id="menu" class="max-h-none w-64 h-screen">
|
<div class="h-12 fixed top-12 mr-4 inset-0 bg-linear-0 from-transparent to-white"></div>
|
||||||
<header class="sticky top-0 z-50 bg-white flex items-center justify-between p-4 border-b-1 border-gray-200">
|
// Content
|
||||||
<a href="/" class="flex gap-2 -mt-2">
|
<main class="flex-1 overflow-auto">
|
||||||
<img width="150" src="/static/logo.svg" alt="SpendSparrow logo"/>
|
if slot != nil {
|
||||||
</a>
|
@slot
|
||||||
<button id="menuButtonClose" class="h-6 w-6">
|
}
|
||||||
@svg.Cancel()
|
</main>
|
||||||
</button>
|
// Footer
|
||||||
</header>
|
<!-- </div> -->
|
||||||
@navigation(path)
|
|
||||||
</dialog>
|
|
||||||
<div id="toasts" class="fixed bottom-4 right-4 ml-4 max-w-96 flex flex-col gap-2 z-50">
|
<div id="toasts" class="fixed bottom-4 right-4 ml-4 max-w-96 flex flex-col gap-2 z-50">
|
||||||
<div
|
<div
|
||||||
id="toast"
|
id="toast"
|
||||||
class="transition-all duration-300
|
class="transition-all duration-300 opacity-0 px-4 py-2 text-lg hidden text-bold rounded bg-amber-900 text-white"
|
||||||
opacity-0 px-4 py-2 text-lg hidden text-bold rounded bg-amber-900 text-white"
|
>
|
||||||
></div>
|
M
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ navigation(path string) {
|
|
||||||
<nav class="w-64 text-nowrap flex gap-2 flex-col text-lg mt-5 px-5 pt-2">
|
|
||||||
<a class={ layoutLinkClass(path == "/dashboard") } href="/dashboard">Dashboard</a>
|
|
||||||
<a class={ layoutLinkClass(path == "/transaction") } href="/transaction">Transaction</a>
|
|
||||||
<a class={ layoutLinkClass(path == "/treasurechest") } href="/treasurechest">Treasure Chest</a>
|
|
||||||
<a class={ layoutLinkClass(path == "/account") } href="/account">Account</a>
|
|
||||||
</nav>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package template
|
package template
|
||||||
|
|
||||||
templ Index() {
|
templ Index() {
|
||||||
|
<!-- <div class="h-full flex flex-col items-center justify-center"> -->
|
||||||
<div class="h-full flex flex-col items-center justify-center">
|
<div class="h-full flex flex-col items-center justify-center">
|
||||||
<h1 class="flex gap-2 w-full justify-center">
|
<h1 class="flex gap-2 w-full justify-center">
|
||||||
<img width="600" src="/static/logo.svg" alt="SpendSparrow logo"/>
|
<img class="w-24" src="/static/favicon.svg" alt="SpendSparrow logo"/>
|
||||||
|
<span class="text-8xl tracking-tighter font-bold font-pirata">SpendSparrow</span>
|
||||||
</h1>
|
</h1>
|
||||||
<h2 class="text-2xl mt-8 text-gray-800">
|
<h2 class="text-2xl mt-8 text-gray-800">
|
||||||
Spend your <span class="px-2 text-3xl text-yellow-800">treasure</span> on the important
|
Spend your <span class="px-2 text-3xl text-yellow-800">treasure</span> on the important
|
||||||
@@ -31,7 +31,7 @@ templ Save() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
templ Cancel() {
|
templ Cancel() {
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" class="text-gray-500">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" class="h-4 w-4 text-gray-500">
|
||||||
<path fill="currentColor" d="m654 501l346 346l-154 154l-346-346l-346 346L0 847l346-346L0 155L154 1l346 346L846 1l154 154z"></path>
|
<path fill="currentColor" d="m654 501l346 346l-154 154l-346-346l-346 346L0 847l346-346L0 155L154 1l346 346L846 1l154 154z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
@@ -47,13 +47,3 @@ templ Info() {
|
|||||||
<path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipSInfo0)"></path>
|
<path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipSInfo0)"></path>
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Menu() {
|
|
||||||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="text-gray-500">
|
|
||||||
<g data-name="1" id="_1">
|
|
||||||
<path d="M441.13,166.52h-372a15,15,0,1,1,0-30h372a15,15,0,0,1,0,30Z"></path>
|
|
||||||
<path d="M441.13,279.72h-372a15,15,0,1,1,0-30h372a15,15,0,0,1,0,30Z"></path>
|
|
||||||
<path d="M441.13,392.92h-372a15,15,0,1,1,0-30h372a15,15,0,0,1,0,30Z"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ templ Transaction(items templ.Component, filter types.TransactionItemsFilter, ac
|
|||||||
<div class="max-w-6xl mt-10 mx-auto">
|
<div class="max-w-6xl mt-10 mx-auto">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<form
|
<form
|
||||||
id="transactionFilterForm"
|
|
||||||
hx-get="/transaction"
|
hx-get="/transaction"
|
||||||
hx-target="#transaction-items"
|
hx-target="#transaction-items"
|
||||||
hx-push-url="true"
|
hx-push-url="true"
|
||||||
@@ -53,7 +52,6 @@ templ Transaction(items templ.Component, filter types.TransactionItemsFilter, ac
|
|||||||
selected?={ filter.Error == "false" }
|
selected?={ filter.Error == "false" }
|
||||||
>Has no Errors</option>
|
>Has no Errors</option>
|
||||||
</select>
|
</select>
|
||||||
<input id="page" name="page" type="hidden" value={ filter.Page }/>
|
|
||||||
</form>
|
</form>
|
||||||
<button
|
<button
|
||||||
hx-get="/transaction/new"
|
hx-get="/transaction/new"
|
||||||
@@ -65,25 +63,7 @@ templ Transaction(items templ.Component, filter types.TransactionItemsFilter, ac
|
|||||||
<p>New Transaction</p>
|
<p>New Transaction</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end items-center gap-5 mt-5">
|
|
||||||
<button id="pagePrev1" class="text-2xl p-2 text-yellow-700 font-black hover:bg-gray-200 rounded-lg decoration-yellow-400 decoration-[0.25rem] hover:underline">
|
|
||||||
<
|
|
||||||
</button>
|
|
||||||
<span class="text-gray-400 text-sm">Page: <span class="text-gray-800 text-xl" id="page1">{ getPageNumber(filter.Page) }</span></span>
|
|
||||||
<button id="pageNext1" class="text-2xl p-2 text-yellow-700 font-black hover:bg-gray-200 rounded-lg decoration-yellow-400 decoration-[0.25rem] hover:underline">
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
@items
|
@items
|
||||||
<div class="flex justify-end items-center gap-5 mt-5">
|
|
||||||
<button id="pagePrev2" class="text-2xl p-2 text-yellow-700 font-black hover:bg-gray-200 rounded-lg decoration-yellow-400 decoration-[0.25rem] hover:underline">
|
|
||||||
<
|
|
||||||
</button>
|
|
||||||
<span class="text-gray-400 text-sm">Page: <span class="text-gray-800 text-xl" id="page2">{ getPageNumber(filter.Page) }</span></span>
|
|
||||||
<button id="pageNext2" class="text-2xl p-2 text-yellow-700 font-black hover:bg-gray-200 rounded-lg decoration-yellow-400 decoration-[0.25rem] hover:underline">
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +103,7 @@ templ EditTransaction(transaction *types.Transaction, accounts []*types.Account,
|
|||||||
if transaction.TreasureChestId != nil {
|
if transaction.TreasureChestId != nil {
|
||||||
treasureChestId = transaction.TreasureChestId.String()
|
treasureChestId = transaction.TreasureChestId.String()
|
||||||
}
|
}
|
||||||
value = formatFloat(transaction.Value)
|
value = displayBalance(transaction.Value)
|
||||||
|
|
||||||
id = transaction.Id.String()
|
id = transaction.Id.String()
|
||||||
cancelUrl = "/transaction/" + id
|
cancelUrl = "/transaction/" + id
|
||||||
@@ -208,9 +188,7 @@ templ EditTransaction(transaction *types.Transaction, accounts []*types.Account,
|
|||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
class="button button-neglect px-1 flex items-center gap-2"
|
class="button button-neglect px-1 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<span class="h-4 w-4">
|
@svg.Cancel()
|
||||||
@svg.Cancel()
|
|
||||||
</span>
|
|
||||||
<span>
|
<span>
|
||||||
Cancel
|
Cancel
|
||||||
</span>
|
</span>
|
||||||
@@ -272,9 +250,9 @@ templ TransactionItem(transaction *types.Transaction, accounts, treasureChests m
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
if transaction.Value < 0 {
|
if transaction.Value < 0 {
|
||||||
<p class="mr-8 min-w-22 text-right text-red-700">{ types.FormatEuros(transaction.Value) }</p>
|
<p class="mr-8 min-w-22 text-right text-red-700">{ displayBalance(transaction.Value)+" €" }</p>
|
||||||
} else {
|
} else {
|
||||||
<p class="mr-8 w-22 text-right text-green-700">{ types.FormatEuros(transaction.Value) }</p>
|
<p class="mr-8 w-22 text-right text-green-700">{ displayBalance(transaction.Value)+" €" }</p>
|
||||||
}
|
}
|
||||||
<button
|
<button
|
||||||
hx-get={ "/transaction/" + transaction.Id.String() + "?edit=true" }
|
hx-get={ "/transaction/" + transaction.Id.String() + "?edit=true" }
|
||||||
@@ -302,16 +280,11 @@ templ TransactionItem(transaction *types.Transaction, accounts, treasureChests m
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatFloat(balance int64) string {
|
func displayBalance(balance int64) string {
|
||||||
|
|
||||||
euros := float64(balance) / 100
|
euros := float64(balance) / 100
|
||||||
return fmt.Sprintf("%.2f", euros)
|
return fmt.Sprintf("%.2f", euros)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPageNumber(page string) string {
|
func calculateReferences() {
|
||||||
if page == "" {
|
|
||||||
return "1"
|
|
||||||
} else {
|
|
||||||
return page
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ templ TransactionRecurringItem(transactionRecurring *types.TransactionRecurring,
|
|||||||
Every <span class="text-xl">{ transactionRecurring.IntervalMonths }</span> month(s)
|
Every <span class="text-xl">{ transactionRecurring.IntervalMonths }</span> month(s)
|
||||||
</p>
|
</p>
|
||||||
if transactionRecurring.Value < 0 {
|
if transactionRecurring.Value < 0 {
|
||||||
<p class="text-right text-red-700">{ types.FormatEuros(transactionRecurring.Value) }</p>
|
<p class="text-right text-red-700">{ displayBalance(transactionRecurring.Value)+" €" }</p>
|
||||||
} else {
|
} else {
|
||||||
<p class="text-right text-green-700">{ types.FormatEuros(transactionRecurring.Value) }</p>
|
<p class="text-right text-green-700">{ displayBalance(transactionRecurring.Value)+" €" }</p>
|
||||||
}
|
}
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -104,7 +104,7 @@ templ EditTransactionRecurring(transactionRecurring *types.TransactionRecurring,
|
|||||||
}
|
}
|
||||||
party = transactionRecurring.Party
|
party = transactionRecurring.Party
|
||||||
description = transactionRecurring.Description
|
description = transactionRecurring.Description
|
||||||
value = formatFloat(transactionRecurring.Value)
|
value = displayBalance(transactionRecurring.Value)
|
||||||
|
|
||||||
id = transactionRecurring.Id.String()
|
id = transactionRecurring.Id.String()
|
||||||
}
|
}
|
||||||
@@ -193,9 +193,7 @@ templ EditTransactionRecurring(transactionRecurring *types.TransactionRecurring,
|
|||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
class="button button-neglect px-1 flex items-center gap-2"
|
class="button button-neglect px-1 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<span class="h-4 w-4">
|
@svg.Cancel()
|
||||||
@svg.Cancel()
|
|
||||||
</span>
|
|
||||||
<span>
|
<span>
|
||||||
Cancel
|
Cancel
|
||||||
</span>
|
</span>
|
||||||
@@ -203,8 +201,11 @@ templ EditTransactionRecurring(transactionRecurring *types.TransactionRecurring,
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatFloat(balance int64) string {
|
func displayBalance(balance int64) string {
|
||||||
|
|
||||||
euros := float64(balance) / 100
|
euros := float64(balance) / 100
|
||||||
return fmt.Sprintf("%.2f", euros)
|
return fmt.Sprintf("%.2f", euros)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func calculateReferences() {
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package treasurechest
|
package treasurechest
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
import "spend-sparrow/internal/template/svg"
|
import "spend-sparrow/internal/template/svg"
|
||||||
import "spend-sparrow/internal/types"
|
import "spend-sparrow/internal/types"
|
||||||
import "github.com/google/uuid"
|
import "github.com/google/uuid"
|
||||||
@@ -88,9 +89,7 @@ templ EditTreasureChest(treasureChest *types.TreasureChest, parents []*types.Tre
|
|||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
class="button button-neglect px-1 flex items-center gap-2"
|
class="button button-neglect px-1 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<span class="h-4 w-4">
|
@svg.Cancel()
|
||||||
@svg.Cancel()
|
|
||||||
</span>
|
|
||||||
<span>
|
<span>
|
||||||
Cancel
|
Cancel
|
||||||
</span>
|
</span>
|
||||||
@@ -131,14 +130,14 @@ templ TreasureChestItem(treasureChest *types.TreasureChest, monthlySums map[uuid
|
|||||||
<p class="mr-auto">{ treasureChest.Name }</p>
|
<p class="mr-auto">{ treasureChest.Name }</p>
|
||||||
<p class="mr-20 text-gray-600">
|
<p class="mr-20 text-gray-600">
|
||||||
if treasureChest.ParentId != nil {
|
if treasureChest.ParentId != nil {
|
||||||
+ { types.FormatEuros(monthlySums[treasureChest.Id]) } <span class="text-gray-500 text-sm"> per month</span>
|
+ { displayBalance(monthlySums[treasureChest.Id]) } <span class="text-gray-500 text-sm"> per month</span>
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
if treasureChest.ParentId != nil {
|
if treasureChest.ParentId != nil {
|
||||||
if treasureChest.CurrentBalance < 0 {
|
if treasureChest.CurrentBalance < 0 {
|
||||||
<p class="mr-20 min-w-20 text-right text-red-700">{ types.FormatEuros(treasureChest.CurrentBalance) }</p>
|
<p class="mr-20 min-w-20 text-right text-red-700">{ displayBalance(treasureChest.CurrentBalance) }</p>
|
||||||
} else {
|
} else {
|
||||||
<p class="mr-20 min-w-20 text-right text-green-700">{ types.FormatEuros(treasureChest.CurrentBalance) }</p>
|
<p class="mr-20 min-w-20 text-right text-green-700">{ displayBalance(treasureChest.CurrentBalance) }</p>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<a
|
<a
|
||||||
@@ -188,3 +187,9 @@ func filterNoChildNoSelf(nodes []*types.TreasureChest, selfId string) []*types.T
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func displayBalance(balance int64) string {
|
||||||
|
|
||||||
|
euros := float64(balance) / 100
|
||||||
|
return fmt.Sprintf("%.2f €", euros)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type DashboardMonthlySummary struct {
|
|
||||||
Month time.Time
|
|
||||||
// Sum of all Transactions with TreasureChests and no Accounts
|
|
||||||
Savings int64
|
|
||||||
// Sum of all Transactions with Accounts and no TreasureChests
|
|
||||||
Income int64
|
|
||||||
// Sum of all Transactions with Accounts and TreasureChests
|
|
||||||
Expenses int64
|
|
||||||
// Income - Expenses
|
|
||||||
Total int64
|
|
||||||
|
|
||||||
SumOfSavings int64
|
|
||||||
SumOfAccounts int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type DashboardMainChartEntry struct {
|
|
||||||
Day time.Time
|
|
||||||
Value int64
|
|
||||||
Savings int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type DashboardTreasureChest struct {
|
|
||||||
Name string
|
|
||||||
Value int64
|
|
||||||
Children []DashboardTreasureChest
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FormatEuros(balance int64) string {
|
|
||||||
prefix := ""
|
|
||||||
if balance < 0 {
|
|
||||||
prefix = "- "
|
|
||||||
balance = -balance
|
|
||||||
}
|
|
||||||
|
|
||||||
n := float64(balance) / 100
|
|
||||||
s := fmt.Sprintf("%.2f", n) // "1234567.89"
|
|
||||||
|
|
||||||
parts := strings.Split(s, ".")
|
|
||||||
intPart := parts[0]
|
|
||||||
fracPart := parts[1]
|
|
||||||
|
|
||||||
var result strings.Builder
|
|
||||||
numberOfSeperators := len(intPart) % 3
|
|
||||||
if numberOfSeperators == 0 {
|
|
||||||
result.WriteString(intPart)
|
|
||||||
} else {
|
|
||||||
for i := range intPart {
|
|
||||||
if i > 0 && (i-numberOfSeperators)%3 == 0 {
|
|
||||||
result.WriteString(",")
|
|
||||||
}
|
|
||||||
result.WriteByte(intPart[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return prefix + result.String() + "." + fracPart + " €"
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
)
|
)
|
||||||
@@ -27,13 +26,13 @@ type SmtpSettings struct {
|
|||||||
FromName string
|
FromName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSettingsFromEnv(ctx context.Context, env func(string) string) (*Settings, error) {
|
func NewSettingsFromEnv(env func(string) string) (*Settings, error) {
|
||||||
var (
|
var (
|
||||||
smtp *SmtpSettings
|
smtp *SmtpSettings
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if env("SMTP_ENABLED") == "true" {
|
if env("SMTP_ENABLED") == "true" {
|
||||||
smtp, err = getSmtpSettings(ctx, env)
|
smtp, err = getSmtpSettings(env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -47,26 +46,26 @@ func NewSettingsFromEnv(ctx context.Context, env func(string) string) (*Settings
|
|||||||
}
|
}
|
||||||
|
|
||||||
if settings.BaseUrl == "" {
|
if settings.BaseUrl == "" {
|
||||||
slog.ErrorContext(ctx, "BASE_URL must be set")
|
slog.Error("BASE_URL must be set")
|
||||||
return nil, ErrMissingConfig
|
return nil, ErrMissingConfig
|
||||||
}
|
}
|
||||||
if settings.Port == "" {
|
if settings.Port == "" {
|
||||||
slog.ErrorContext(ctx, "PORT must be set")
|
slog.Error("PORT must be set")
|
||||||
return nil, ErrMissingConfig
|
return nil, ErrMissingConfig
|
||||||
}
|
}
|
||||||
if settings.Environment == "" {
|
if settings.Environment == "" {
|
||||||
slog.ErrorContext(ctx, "ENVIRONMENT must be set")
|
slog.Error("ENVIRONMENT must be set")
|
||||||
return nil, ErrMissingConfig
|
return nil, ErrMissingConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.InfoContext(ctx, "settings read", "BASE_URL", settings.BaseUrl)
|
slog.Info("settings read", "BASE_URL", settings.BaseUrl)
|
||||||
slog.InfoContext(ctx, "settings read", "ENVIRONMENT", settings.Environment)
|
slog.Info("settings read", "ENVIRONMENT", settings.Environment)
|
||||||
slog.InfoContext(ctx, "settings read", "ENVIRONMENT", settings.Environment)
|
slog.Info("settings read", "ENVIRONMENT", settings.Environment)
|
||||||
|
|
||||||
return settings, nil
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSmtpSettings(ctx context.Context, env func(string) string) (*SmtpSettings, error) {
|
func getSmtpSettings(env func(string) string) (*SmtpSettings, error) {
|
||||||
smtp := SmtpSettings{
|
smtp := SmtpSettings{
|
||||||
Host: env("SMTP_HOST"),
|
Host: env("SMTP_HOST"),
|
||||||
Port: env("SMTP_PORT"),
|
Port: env("SMTP_PORT"),
|
||||||
@@ -77,27 +76,27 @@ func getSmtpSettings(ctx context.Context, env func(string) string) (*SmtpSetting
|
|||||||
}
|
}
|
||||||
|
|
||||||
if smtp.Host == "" {
|
if smtp.Host == "" {
|
||||||
slog.ErrorContext(ctx, "SMTP_HOST must be set")
|
slog.Error("SMTP_HOST must be set")
|
||||||
return nil, ErrMissingConfig
|
return nil, ErrMissingConfig
|
||||||
}
|
}
|
||||||
if smtp.Port == "" {
|
if smtp.Port == "" {
|
||||||
slog.ErrorContext(ctx, "SMTP_PORT must be set")
|
slog.Error("SMTP_PORT must be set")
|
||||||
return nil, ErrMissingConfig
|
return nil, ErrMissingConfig
|
||||||
}
|
}
|
||||||
if smtp.User == "" {
|
if smtp.User == "" {
|
||||||
slog.ErrorContext(ctx, "SMTP_USER must be set")
|
slog.Error("SMTP_USER must be set")
|
||||||
return nil, ErrMissingConfig
|
return nil, ErrMissingConfig
|
||||||
}
|
}
|
||||||
if smtp.Pass == "" {
|
if smtp.Pass == "" {
|
||||||
slog.ErrorContext(ctx, "SMTP_PASS must be set")
|
slog.Error("SMTP_PASS must be set")
|
||||||
return nil, ErrMissingConfig
|
return nil, ErrMissingConfig
|
||||||
}
|
}
|
||||||
if smtp.FromMail == "" {
|
if smtp.FromMail == "" {
|
||||||
slog.ErrorContext(ctx, "SMTP_FROM_MAIL must be set")
|
slog.Error("SMTP_FROM_MAIL must be set")
|
||||||
return nil, ErrMissingConfig
|
return nil, ErrMissingConfig
|
||||||
}
|
}
|
||||||
if smtp.FromName == "" {
|
if smtp.FromName == "" {
|
||||||
slog.ErrorContext(ctx, "SMTP_FROM_NAME must be set")
|
slog.Error("SMTP_FROM_NAME must be set")
|
||||||
return nil, ErrMissingConfig
|
return nil, ErrMissingConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,26 +6,13 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Transaction is at the center of the application.
|
// At the center of the application is the transaction.
|
||||||
//
|
//
|
||||||
// Every piece of data should be calculated based on transactions.
|
// Every piece of data should be calculated based on transactions.
|
||||||
// This means potential calculation errors can be fixed later in time.
|
// This means potential calculation errors can be fixed later in time.
|
||||||
//
|
//
|
||||||
// If it becomes necessary to precalculate snapshots for performance reasons, this can be done in the future.
|
// If it becomes necessary to precalculate snapshots for performance reasons, this can be done in the future.
|
||||||
// But the transaction should always be the source of truth.
|
// But the transaction should always be the source of truth.
|
||||||
//
|
|
||||||
// There are the following constallations and their explanation:
|
|
||||||
//
|
|
||||||
// Account | TreasureChest | Value | Description
|
|
||||||
// --------|---------------|-------|----------------
|
|
||||||
// Y | Y | + | Invalid
|
|
||||||
// Y | Y | - | Expense
|
|
||||||
// Y | N | + | Deposit
|
|
||||||
// Y | N | - | Withdrawal (for moving between accounts)
|
|
||||||
// N | Y | + | Saving
|
|
||||||
// N | Y | - | Withdrawal (for moving between treasure chests)
|
|
||||||
// N | N | + | Invalid
|
|
||||||
// N | N | - | Invalid
|
|
||||||
type Transaction struct {
|
type Transaction struct {
|
||||||
Id uuid.UUID `db:"id"`
|
Id uuid.UUID `db:"id"`
|
||||||
UserId uuid.UUID `db:"user_id"`
|
UserId uuid.UUID `db:"user_id"`
|
||||||
@@ -51,5 +38,4 @@ type TransactionItemsFilter struct {
|
|||||||
AccountId string
|
AccountId string
|
||||||
TreasureChestId string
|
TreasureChestId string
|
||||||
Error string
|
Error string
|
||||||
Page string
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -9,16 +8,16 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TriggerToast(ctx context.Context, w http.ResponseWriter, r *http.Request, class string, message string) {
|
func TriggerToast(w http.ResponseWriter, r *http.Request, class string, message string) {
|
||||||
if IsHtmx(r) {
|
if IsHtmx(r) {
|
||||||
w.Header().Set("Hx-Trigger", fmt.Sprintf(`{"toast": "%v|%v"}`, class, strings.ReplaceAll(message, `"`, `\"`)))
|
w.Header().Set("Hx-Trigger", fmt.Sprintf(`{"toast": "%v|%v"}`, class, strings.ReplaceAll(message, `"`, `\"`)))
|
||||||
} else {
|
} else {
|
||||||
slog.ErrorContext(ctx, "Trying to trigger toast in non-HTMX request")
|
slog.Error("Trying to trigger toast in non-HTMX request")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TriggerToastWithStatus(ctx context.Context, w http.ResponseWriter, r *http.Request, class string, message string, statusCode int) {
|
func TriggerToastWithStatus(w http.ResponseWriter, r *http.Request, class string, message string, statusCode int) {
|
||||||
TriggerToast(ctx, w, r, class, message)
|
TriggerToast(w, r, class, message)
|
||||||
w.WriteHeader(statusCode)
|
w.WriteHeader(statusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ func DoRedirect(w http.ResponseWriter, r *http.Request, url string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WaitMinimumTime[T any](waitTime time.Duration, f func() (T, error)) (T, error) {
|
func WaitMinimumTime[T interface{}](waitTime time.Duration, f func() (T, error)) (T, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
result, err := f()
|
result, err := f()
|
||||||
time.Sleep(waitTime - time.Since(start))
|
time.Sleep(waitTime - time.Since(start))
|
||||||
|
|||||||
17
main.go
17
main.go
@@ -6,36 +6,31 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"spend-sparrow/internal"
|
"spend-sparrow/internal"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"github.com/uptrace/opentelemetry-go-extra/otelsql"
|
|
||||||
"github.com/uptrace/opentelemetry-go-extra/otelsqlx"
|
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
err := godotenv.Load()
|
err := godotenv.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Error loading .env file")
|
slog.Error("Error loading .env file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := otelsqlx.Open("sqlite3", "./data/spend-sparrow.db?_journal_mode=WAL",
|
db, err := sqlx.Open("sqlite3", "./data/spend-sparrow.db")
|
||||||
otelsql.WithAttributes(semconv.DBSystemSqlite))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "Could not open Database data.db", "err", err)
|
slog.Error("Could not open Database data.db", "err", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err = db.Close(); err != nil {
|
if err = db.Close(); err != nil {
|
||||||
slog.ErrorContext(ctx, "Database close failed", "err", err)
|
slog.Error("Database close failed", "err", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err = internal.Run(context.Background(), db, "", os.Getenv); err != nil {
|
if err = internal.Run(context.Background(), db, "", os.Getenv); err != nil {
|
||||||
slog.ErrorContext(ctx, "Error running server", "err", err)
|
slog.Error("Error running server", "err", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
513
package-lock.json
generated
513
package-lock.json
generated
@@ -9,32 +9,51 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/cli": "4.1.16",
|
"@tailwindcss/cli": "4.1.8",
|
||||||
"echarts": "6.0.0",
|
"htmx.org": "2.0.4",
|
||||||
"htmx.org": "2.0.8",
|
"tailwindcss": "4.1.8"
|
||||||
"tailwindcss": "4.1.16"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
"version": "0.3.13",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
|
||||||
"@jridgewell/trace-mapping": "^0.3.24"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/remapping": {
|
|
||||||
"version": "2.3.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
|
||||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
"@jridgewell/trace-mapping": "^0.3.24"
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@isaacs/fs-minipass": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^7.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
|
"version": "0.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||||
|
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/set-array": "^1.2.1",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
@@ -47,17 +66,27 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jridgewell/set-array": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.30",
|
"version": "0.3.25",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||||
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
|
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -375,68 +404,73 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/cli": {
|
"node_modules/@tailwindcss/cli": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.8.tgz",
|
||||||
"integrity": "sha512-dsnANPrh2ZooHyZ/8uJhc9ecpcYtufToc21NY09NS9vF16rxPCjJ8dP7TUAtPqlUJTHSmRkN2hCdoYQIlgh4fw==",
|
"integrity": "sha512-+6lkjXSr/68zWiabK3mVYVHmOq/SAHjJ13mR8spyB4LgUWZbWzU9kCSErlAUo+gK5aVfgqe8kY6Ltz9+nz5XYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@parcel/watcher": "^2.5.1",
|
"@parcel/watcher": "^2.5.1",
|
||||||
"@tailwindcss/node": "4.1.16",
|
"@tailwindcss/node": "4.1.8",
|
||||||
"@tailwindcss/oxide": "4.1.16",
|
"@tailwindcss/oxide": "4.1.8",
|
||||||
"enhanced-resolve": "^5.18.3",
|
"enhanced-resolve": "^5.18.1",
|
||||||
"mri": "^1.2.0",
|
"mri": "^1.2.0",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"tailwindcss": "4.1.16"
|
"tailwindcss": "4.1.8"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"tailwindcss": "dist/index.mjs"
|
"tailwindcss": "dist/index.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.8.tgz",
|
||||||
"integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==",
|
"integrity": "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/remapping": "^2.3.4",
|
"@ampproject/remapping": "^2.3.0",
|
||||||
"enhanced-resolve": "^5.18.3",
|
"enhanced-resolve": "^5.18.1",
|
||||||
"jiti": "^2.6.1",
|
"jiti": "^2.4.2",
|
||||||
"lightningcss": "1.30.2",
|
"lightningcss": "1.30.1",
|
||||||
"magic-string": "^0.30.19",
|
"magic-string": "^0.30.17",
|
||||||
"source-map-js": "^1.2.1",
|
"source-map-js": "^1.2.1",
|
||||||
"tailwindcss": "4.1.16"
|
"tailwindcss": "4.1.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide": {
|
"node_modules/@tailwindcss/oxide": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.8.tgz",
|
||||||
"integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==",
|
"integrity": "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.4",
|
||||||
|
"tar": "^7.4.3"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tailwindcss/oxide-android-arm64": "4.1.16",
|
"@tailwindcss/oxide-android-arm64": "4.1.8",
|
||||||
"@tailwindcss/oxide-darwin-arm64": "4.1.16",
|
"@tailwindcss/oxide-darwin-arm64": "4.1.8",
|
||||||
"@tailwindcss/oxide-darwin-x64": "4.1.16",
|
"@tailwindcss/oxide-darwin-x64": "4.1.8",
|
||||||
"@tailwindcss/oxide-freebsd-x64": "4.1.16",
|
"@tailwindcss/oxide-freebsd-x64": "4.1.8",
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16",
|
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8",
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.16",
|
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.8",
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.16",
|
"@tailwindcss/oxide-linux-arm64-musl": "4.1.8",
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.16",
|
"@tailwindcss/oxide-linux-x64-gnu": "4.1.8",
|
||||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.16",
|
"@tailwindcss/oxide-linux-x64-musl": "4.1.8",
|
||||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.16",
|
"@tailwindcss/oxide-wasm32-wasi": "4.1.8",
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.16",
|
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.8",
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.16"
|
"@tailwindcss/oxide-win32-x64-msvc": "4.1.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.8.tgz",
|
||||||
"integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==",
|
"integrity": "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -451,9 +485,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.8.tgz",
|
||||||
"integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==",
|
"integrity": "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -468,9 +502,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.8.tgz",
|
||||||
"integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==",
|
"integrity": "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -485,9 +519,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.8.tgz",
|
||||||
"integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==",
|
"integrity": "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -502,9 +536,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.8.tgz",
|
||||||
"integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==",
|
"integrity": "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -519,9 +553,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.8.tgz",
|
||||||
"integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==",
|
"integrity": "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -536,9 +570,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.8.tgz",
|
||||||
"integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==",
|
"integrity": "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -553,9 +587,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.8.tgz",
|
||||||
"integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==",
|
"integrity": "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -570,9 +604,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.8.tgz",
|
||||||
"integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==",
|
"integrity": "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -587,9 +621,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.8.tgz",
|
||||||
"integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==",
|
"integrity": "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==",
|
||||||
"bundleDependencies": [
|
"bundleDependencies": [
|
||||||
"@napi-rs/wasm-runtime",
|
"@napi-rs/wasm-runtime",
|
||||||
"@emnapi/core",
|
"@emnapi/core",
|
||||||
@@ -605,30 +639,30 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/core": "^1.5.0",
|
"@emnapi/core": "^1.4.3",
|
||||||
"@emnapi/runtime": "^1.5.0",
|
"@emnapi/runtime": "^1.4.3",
|
||||||
"@emnapi/wasi-threads": "^1.1.0",
|
"@emnapi/wasi-threads": "^1.0.2",
|
||||||
"@napi-rs/wasm-runtime": "^1.0.7",
|
"@napi-rs/wasm-runtime": "^0.2.10",
|
||||||
"@tybys/wasm-util": "^0.10.1",
|
"@tybys/wasm-util": "^0.9.0",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.8.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||||
"version": "1.5.0",
|
"version": "1.4.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"inBundle": true,
|
"inBundle": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/wasi-threads": "1.1.0",
|
"@emnapi/wasi-threads": "1.0.2",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||||
"version": "1.5.0",
|
"version": "1.4.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"inBundle": true,
|
"inBundle": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -638,7 +672,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.1.0",
|
"version": "1.0.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"inBundle": true,
|
"inBundle": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -648,19 +682,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.0.7",
|
"version": "0.2.10",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"inBundle": true,
|
"inBundle": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/core": "^1.5.0",
|
"@emnapi/core": "^1.4.3",
|
||||||
"@emnapi/runtime": "^1.5.0",
|
"@emnapi/runtime": "^1.4.3",
|
||||||
"@tybys/wasm-util": "^0.10.1"
|
"@tybys/wasm-util": "^0.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.1",
|
"version": "0.9.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"inBundle": true,
|
"inBundle": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -670,16 +704,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"inBundle": true,
|
"inBundle": true,
|
||||||
"license": "0BSD",
|
"license": "0BSD",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.8.tgz",
|
||||||
"integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==",
|
"integrity": "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -694,9 +728,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.8.tgz",
|
||||||
"integrity": "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==",
|
"integrity": "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -710,6 +744,16 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide/node_modules/detect-libc": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
@@ -723,6 +767,16 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chownr": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||||
@@ -736,21 +790,10 @@
|
|||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/echarts": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "2.3.0",
|
|
||||||
"zrender": "6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.3",
|
"version": "5.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||||
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -782,9 +825,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/htmx.org": {
|
"node_modules/htmx.org": {
|
||||||
"version": "2.0.8",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.4.tgz",
|
||||||
"integrity": "sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q==",
|
"integrity": "sha512-HLxMCdfXDOJirs3vBZl/ZLoY+c7PfM4Ahr2Ad4YXh6d22T5ltbTXFFkpx9Tgb2vvmWFMbIc3LqN2ToNkZJvyYQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
@@ -822,9 +865,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "2.6.1",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -832,9 +875,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||||
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -848,44 +891,22 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"lightningcss-android-arm64": "1.30.2",
|
"lightningcss-darwin-arm64": "1.30.1",
|
||||||
"lightningcss-darwin-arm64": "1.30.2",
|
"lightningcss-darwin-x64": "1.30.1",
|
||||||
"lightningcss-darwin-x64": "1.30.2",
|
"lightningcss-freebsd-x64": "1.30.1",
|
||||||
"lightningcss-freebsd-x64": "1.30.2",
|
"lightningcss-linux-arm-gnueabihf": "1.30.1",
|
||||||
"lightningcss-linux-arm-gnueabihf": "1.30.2",
|
"lightningcss-linux-arm64-gnu": "1.30.1",
|
||||||
"lightningcss-linux-arm64-gnu": "1.30.2",
|
"lightningcss-linux-arm64-musl": "1.30.1",
|
||||||
"lightningcss-linux-arm64-musl": "1.30.2",
|
"lightningcss-linux-x64-gnu": "1.30.1",
|
||||||
"lightningcss-linux-x64-gnu": "1.30.2",
|
"lightningcss-linux-x64-musl": "1.30.1",
|
||||||
"lightningcss-linux-x64-musl": "1.30.2",
|
"lightningcss-win32-arm64-msvc": "1.30.1",
|
||||||
"lightningcss-win32-arm64-msvc": "1.30.2",
|
"lightningcss-win32-x64-msvc": "1.30.1"
|
||||||
"lightningcss-win32-x64-msvc": "1.30.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lightningcss-android-arm64": {
|
|
||||||
"version": "1.30.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
|
|
||||||
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/parcel"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-arm64": {
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
|
||||||
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
|
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -904,9 +925,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-x64": {
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
|
||||||
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
|
"integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -925,9 +946,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-freebsd-x64": {
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
|
||||||
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
|
"integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -946,9 +967,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
|
||||||
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
|
"integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -967,9 +988,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
|
||||||
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
|
"integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -988,9 +1009,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-musl": {
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
|
||||||
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
|
"integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1009,9 +1030,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-gnu": {
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
|
||||||
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
|
"integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1030,9 +1051,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-musl": {
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
|
||||||
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
|
"integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1051,9 +1072,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
|
||||||
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
|
"integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1072,9 +1093,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-x64-msvc": {
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
|
||||||
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
|
"integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1093,9 +1114,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss/node_modules/detect-libc": {
|
"node_modules/lightningcss/node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1103,13 +1124,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.19",
|
"version": "0.30.17",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||||
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
|
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
@@ -1126,6 +1147,45 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minipass": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minizlib": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^7.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mkdirp": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"mkdirp": "dist/cjs/src/bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mri": {
|
"node_modules/mri": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||||
@@ -1174,22 +1234,40 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz",
|
||||||
"integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==",
|
"integrity": "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||||
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
|
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tar": {
|
||||||
|
"version": "7.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
|
||||||
|
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/fs-minipass": "^4.0.0",
|
||||||
|
"chownr": "^3.0.0",
|
||||||
|
"minipass": "^7.1.2",
|
||||||
|
"minizlib": "^3.0.1",
|
||||||
|
"mkdirp": "^3.0.1",
|
||||||
|
"yallist": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
@@ -1203,21 +1281,14 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/yallist": {
|
||||||
"version": "2.3.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "0BSD"
|
"license": "BlueOak-1.0.0",
|
||||||
},
|
"engines": {
|
||||||
"node_modules/zrender": {
|
"node": ">=18"
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "2.3.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -1,19 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "spend-sparrow",
|
"name": "spend-sparrow",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Personal finance tracking done right",
|
"description": "Your (almost) independent tech stack to host on a VPC.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && cp -f node_modules/echarts/dist/echarts.min.js static/js/echarts.min.js && tailwindcss -i input.css -o static/css/tailwind.css --minify",
|
"build": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --minify",
|
||||||
"watch": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && cp -f node_modules/echarts/dist/echarts.min.js static/js/echarts.min.js && tailwindcss -i input.css -o static/css/tailwind.css --watch"
|
"watch": "cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --watch"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/cli": "4.1.16",
|
"htmx.org": "2.0.4",
|
||||||
"htmx.org": "2.0.8",
|
"tailwindcss": "4.1.8",
|
||||||
"tailwindcss": "4.1.16",
|
"@tailwindcss/cli": "4.1.8"
|
||||||
"echarts": "6.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 128 128"><path d="M93.46 39.45c6.71-1.49 15.45-8.15 16.78-11.43.78-1.92-3.11-4.92-4.15-6.13-2.38-2.76-1.42-4.12-.5-7.41 1.05-3.74-1.44-7.87-4.97-9.49s-7.75-1.11-11.3.47-6.58 4.12-9.55 6.62c-2.17-1.37-5.63-7.42-11.23-3.49-3.87 2.71-4.22 8.61-3.72 13.32 1.17 10.87 3.85 16.51 8.9 18.03 6.38 1.92 13.44.91 19.74-.49" style="fill:#ffca28"/><path d="M104.36 8.18c-.85 14.65-15.14 24.37-21.92 28.65l4.4 3.78s2.79.06 6.61-1.16c6.55-2.08 16.12-7.96 16.78-11.43.97-5.05-4.21-3.95-5.38-7.94-.61-2.11 2.97-6.1-.49-11.9M79.78 12.09s-2.55-2.61-4.44-3.8c-.94 1.77-1.61 3.69-1.94 5.67-.59 3.48 0 8.42 1.39 12.1.22.57 1.04.48 1.13-.12 1.2-7.91 3.86-13.85 3.86-13.85" style="fill:#e2a610"/><path d="M61.96 38.16S30.77 41.53 16.7 68.61s-2.11 43.5 10.55 49.48 44.56 8.09 65.31 3.17 25.94-15.12 24.97-24.97c-1.41-14.38-14.77-23.22-14.77-23.22s.53-17.76-13.25-29.29c-12.23-10.24-27.55-5.62-27.55-5.62" style="fill:#ffca28"/><path d="M74.76 83.73c-6.69-8.44-14.59-9.57-17.12-12.6-1.38-1.65-2.19-3.32-1.88-5.39.33-2.2 2.88-3.72 4.86-4.09 2.31-.44 7.82-.21 12.45 4.2 1.1 1.04.7 2.66.67 4.11-.08 3.11 4.37 6.13 7.97 3.53 3.61-2.61.84-8.42-1.49-11.24-1.76-2.13-8.14-6.82-16.07-7.56-2.23-.21-11.2-1.54-16.38 8.31-1.49 2.83-2.04 9.67 5.76 15.45 1.63 1.21 10.09 5.51 12.44 8.3 4.07 4.83 1.28 9.08-1.9 9.64-8.67 1.52-13.58-3.17-14.49-5.74-.65-1.83.03-3.81-.81-5.53-.86-1.77-2.62-2.47-4.48-1.88-6.1 1.94-4.16 8.61-1.46 12.28 2.89 3.93 6.44 6.3 10.43 7.6 14.89 4.85 22.05-2.81 23.3-8.42.92-4.11.82-7.67-1.8-10.97" style="fill:#6b4b46"/><path d="M71.16 48.99c-12.67 27.06-14.85 61.23-14.85 61.23" style="fill:none;stroke:#6b4b46;stroke-width:5;stroke-miterlimit:10"/><path d="M81.67 31.96c8.44 2.75 10.31 10.38 9.7 12.46-.73 2.44-10.08-7.06-23.98-6.49-4.86.2-3.45-2.78-1.2-4.5 2.97-2.27 7.96-3.91 15.48-1.47" style="fill:#6d4c41"/><path d="M81.67 31.96c8.44 2.75 10.31 10.38 9.7 12.46-.73 2.44-10.08-7.06-23.98-6.49-4.86.2-3.45-2.78-1.2-4.5 2.97-2.27 7.96-3.91 15.48-1.47" style="fill:#6b4b46"/><path d="M96.49 58.86c1.06-.73 4.62.53 5.62 7.5.49 3.41.64 6.71.64 6.71s-4.2-3.77-5.59-6.42c-1.75-3.35-2.43-6.59-.67-7.79" style="fill:#e2a610"/></svg>
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="28.384802mm"
|
|
||||||
height="31.749905mm"
|
|
||||||
viewBox="0 0 28.384802 31.749905"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs1" />
|
|
||||||
<g
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-37.253301,-88.598061)">
|
|
||||||
<path
|
|
||||||
d="m 59.240389,97.978247 c 1.775354,-0.394229 4.087813,-2.156354 4.439709,-3.024187 0.206375,-0.508 -0.822855,-1.30175 -1.098021,-1.621896 -0.629709,-0.73025 -0.375709,-1.090083 -0.132292,-1.960562 0.277813,-0.989542 -0.381,-2.082271 -1.314979,-2.510896 -0.933979,-0.428625 -2.050521,-0.293688 -2.989792,0.124354 -0.939271,0.418042 -1.740958,1.090083 -2.526771,1.751542 -0.574145,-0.36248 -1.489604,-1.963209 -2.97127,-0.923396 -1.023938,0.717021 -1.116542,2.278062 -0.98425,3.52425 0.309562,2.876021 1.018645,4.368271 2.354791,4.770437 1.688042,0.508 3.556,0.240771 5.222875,-0.129646"
|
|
||||||
style="fill:#ffca28;stroke-width:0.264583"
|
|
||||||
id="path1-3" />
|
|
||||||
<path
|
|
||||||
d="m 62.124348,89.704727 c -0.224896,3.876145 -4.005792,6.447895 -5.799667,7.580312 l 1.164167,1.000125 c 0,0 0.738187,0.01588 1.748895,-0.306917 1.733021,-0.550333 4.265084,-2.106083 4.439709,-3.024187 0.256646,-1.336146 -1.113896,-1.045104 -1.423459,-2.100792 -0.161395,-0.558271 0.785813,-1.613958 -0.129645,-3.148541 m -6.503459,1.03452 c 0,0 -0.674687,-0.690562 -1.17475,-1.005416 -0.248708,0.468312 -0.425979,0.976312 -0.513291,1.500187 -0.156105,0.92075 0,2.227792 0.36777,3.201459 0.05821,0.150812 0.275167,0.127 0.29898,-0.03175 0.3175,-2.092855 1.021291,-3.66448 1.021291,-3.66448"
|
|
||||||
style="fill:#e2a610;stroke-width:0.264583"
|
|
||||||
id="path2-6" />
|
|
||||||
<path
|
|
||||||
d="m 50.906014,97.636935 c 0,0 -8.252354,0.891646 -11.975042,8.056565 -3.722687,7.16492 -0.558271,11.50937 2.791354,13.09158 3.349626,1.58221 11.789834,2.14048 17.279938,0.83873 5.490104,-1.30175 6.863292,-4.0005 6.606646,-6.60664 -0.373062,-3.80471 -3.907896,-6.14363 -3.907896,-6.14363 0,0 0.140229,-4.699 -3.505729,-7.749647 -3.235854,-2.709333 -7.289271,-1.486958 -7.289271,-1.486958"
|
|
||||||
style="fill:#ffca28;stroke-width:0.264583"
|
|
||||||
id="path3-0" />
|
|
||||||
<path
|
|
||||||
d="m 56.120952,95.996518 c 2.233083,0.727604 2.727854,2.746375 2.566458,3.296709 -0.193146,0.645583 -2.667,-1.867959 -6.344708,-1.717146 -1.285875,0.05292 -0.912813,-0.735542 -0.3175,-1.190625 0.785812,-0.600604 2.106083,-1.034521 4.09575,-0.388938"
|
|
||||||
style="fill:#6d4c41;stroke-width:0.264583"
|
|
||||||
id="path6-6" />
|
|
||||||
<path
|
|
||||||
d="m 56.120952,95.996518 c 2.233083,0.727604 2.727854,2.746375 2.566458,3.296709 -0.193146,0.645583 -2.667,-1.867959 -6.344708,-1.717146 -1.285875,0.05292 -0.912813,-0.735542 -0.3175,-1.190625 0.785812,-0.600604 2.106083,-1.034521 4.09575,-0.388938"
|
|
||||||
style="fill:#6b4b46;stroke-width:0.264583"
|
|
||||||
id="path7-2" />
|
|
||||||
<path
|
|
||||||
d="m 60.042077,103.11381 c 0.280458,-0.19314 1.222375,0.14023 1.486958,1.98438 0.129646,0.90223 0.169333,1.77535 0.169333,1.77535 0,0 -1.11125,-0.99748 -1.47902,-1.69862 -0.463021,-0.88636 -0.642938,-1.74361 -0.177271,-2.06111"
|
|
||||||
style="fill:#e2a610;stroke-width:0.264583"
|
|
||||||
id="path8-6" />
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
|
|
||||||
x="82.355011"
|
|
||||||
y="90.66716"
|
|
||||||
id="text4-9"
|
|
||||||
transform="rotate(20.578693)"><tspan
|
|
||||||
id="tspan4-2"
|
|
||||||
style="font-size:19.7556px;fill:#4d4d4d;stroke-width:0.264583"
|
|
||||||
x="82.355011"
|
|
||||||
y="90.66716">$</tspan></text>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 2.1 KiB |
BIN
static/font/PirataOne-Regular.woff2
Normal file
BIN
static/font/PirataOne-Regular.woff2
Normal file
Binary file not shown.
@@ -1,100 +0,0 @@
|
|||||||
// Initialize the echarts instance based on the prepared dom
|
|
||||||
|
|
||||||
async function initMainChart() {
|
|
||||||
const element = document.getElementById('main-chart')
|
|
||||||
if (element === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var myChart = echarts.init(element);
|
|
||||||
window.addEventListener('resize', function() {
|
|
||||||
myChart.resize();
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("/dashboard/main-chart");
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Response status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const option = await response.json();
|
|
||||||
option.tooltip.formatter = function (params) {
|
|
||||||
return new Date(params[0].data[0]).toLocaleString([], { day: 'numeric', month: 'short', year: 'numeric' }) +
|
|
||||||
'<br />' +
|
|
||||||
'Sum of Accounts: <span class="font-bold">' + params[0].data[1] + '</span> € <br />' +
|
|
||||||
'Sum of Savings: <span class="font-bold">' + params[1].data[1] + '</span> €'
|
|
||||||
};
|
|
||||||
|
|
||||||
myChart.setOption(option);
|
|
||||||
|
|
||||||
console.log("initialized main-chart");
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initTreasureChests() {
|
|
||||||
const element = document.getElementById('treasure-chests')
|
|
||||||
if (element === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var myChart = echarts.init(element);
|
|
||||||
window.addEventListener('resize', function() {
|
|
||||||
myChart.resize();
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("/dashboard/treasure-chests");
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Response status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const option = await response.json();
|
|
||||||
myChart.setOption(option);
|
|
||||||
|
|
||||||
console.log("initialized treasure-chests");
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initTreasureChest() {
|
|
||||||
const element = document.getElementById('treasure-chest')
|
|
||||||
if (element === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var myChart = echarts.init(element);
|
|
||||||
window.addEventListener('resize', function() {
|
|
||||||
myChart.resize();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const treasureChestSelect = document.getElementById('treasure-chest-id')
|
|
||||||
treasureChestSelect.addEventListener("change", async (e) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/dashboard/treasure-chest?id="+e.target.value);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Response status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const option = await response.json();
|
|
||||||
option.tooltip.formatter = function (params) {
|
|
||||||
return new Date(params[0].data[0]).toLocaleString([], { day: 'numeric', month: 'short', year: 'numeric' }) +
|
|
||||||
'<br />' +
|
|
||||||
'Sum of Accounts: <span class="font-bold">' + params[0].data[1] + '</span> €'
|
|
||||||
};
|
|
||||||
|
|
||||||
myChart.setOption(option);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("initialized treasure-chest");
|
|
||||||
}
|
|
||||||
|
|
||||||
initMainChart();
|
|
||||||
initTreasureChests();
|
|
||||||
initTreasureChest();
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
menuButton.addEventListener("click", function (e) {
|
|
||||||
menu.showModal();
|
|
||||||
});
|
|
||||||
menuButtonClose.addEventListener("click", function (e) {
|
|
||||||
menu.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
@@ -4,6 +4,7 @@ htmx.on("htmx:afterSwap", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
console.log("DOMContentLoaded");
|
||||||
updateTime(document);
|
updateTime(document);
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -16,7 +17,6 @@ function updateTime() {
|
|||||||
const newDate = value.includes("UTC") ? new Date(value) : value;
|
const newDate = value.includes("UTC") ? new Date(value) : value;
|
||||||
el.valueAsDate = newDate;
|
el.valueAsDate = newDate;
|
||||||
}
|
}
|
||||||
el.classList.remove("datetime");
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
if (!page || !page1 || !pagePrev1 || !pageNext1 || !page2 || !pagePrev2 || !pageNext2 || !transactionFilterForm) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const scrollToTop = function() {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
};
|
|
||||||
const incPage = function() {
|
|
||||||
const currPage = Number(page.value);
|
|
||||||
var nextPage = currPage
|
|
||||||
if (currPage > 1) {
|
|
||||||
nextPage -= 1;
|
|
||||||
page.value = nextPage;
|
|
||||||
transactionFilterForm.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
page1.textContent = nextPage;
|
|
||||||
page2.textContent = nextPage;
|
|
||||||
scrollToTop();
|
|
||||||
};
|
|
||||||
const decPage = function() {
|
|
||||||
const currPage = Number(page.value);
|
|
||||||
var nextPage = currPage + 1;
|
|
||||||
page.value = nextPage;
|
|
||||||
transactionFilterForm.dispatchEvent(new Event('change'));
|
|
||||||
page1.textContent = nextPage;
|
|
||||||
page2.textContent = nextPage;
|
|
||||||
scrollToTop();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pagePrev1.addEventListener("click", incPage);
|
|
||||||
pagePrev2.addEventListener("click", incPage);
|
|
||||||
|
|
||||||
pageNext1.addEventListener("click", decPage);
|
|
||||||
pageNext2.addEventListener("click", decPage);
|
|
||||||
|
|
||||||
console.log("initialized pagination");
|
|
||||||
})
|
|
||||||
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="96.820343mm"
|
|
||||||
height="31.749899mm"
|
|
||||||
viewBox="0 0 96.820343 31.749899"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs1" />
|
|
||||||
<g
|
|
||||||
id="g7"
|
|
||||||
transform="translate(-38.090175,-77.467441)">
|
|
||||||
<g
|
|
||||||
id="g8"
|
|
||||||
transform="translate(-38.797122,-28.178962)">
|
|
||||||
<path
|
|
||||||
d="m 98.874384,115.02659 c 1.775356,-0.39423 4.087816,-2.15635 4.439706,-3.02419 0.20638,-0.508 -0.82285,-1.30175 -1.09802,-1.62189 -0.62971,-0.73025 -0.37571,-1.09009 -0.13229,-1.96057 0.27781,-0.98954 -0.381,-2.08227 -1.31498,-2.51089 -0.933978,-0.42863 -2.05052,-0.29369 -2.989791,0.12435 -0.939271,0.41804 -1.740958,1.09009 -2.526771,1.75154 -0.574145,-0.36248 -1.489604,-1.96321 -2.97127,-0.92339 -1.023938,0.71702 -1.116542,2.27806 -0.98425,3.52425 0.309562,2.87602 1.018645,4.36827 2.354791,4.77043 1.688042,0.508 3.556,0.24078 5.222875,-0.12964"
|
|
||||||
style="fill:#ffca28;stroke-width:0.264583"
|
|
||||||
id="path1-3-3" />
|
|
||||||
<path
|
|
||||||
d="m 101.75834,106.75307 c -0.22489,3.87614 -4.005789,6.44789 -5.799664,7.58031 l 1.164167,1.00013 c 0,0 0.738187,0.0159 1.748895,-0.30692 1.733022,-0.55033 4.265082,-2.10608 4.439712,-3.02419 0.25664,-1.33614 -1.1139,-1.0451 -1.42346,-2.10079 -0.1614,-0.55827 0.78581,-1.61396 -0.12965,-3.14854 m -6.503456,1.03452 c 0,0 -0.674687,-0.69056 -1.17475,-1.00542 -0.248708,0.46832 -0.425979,0.97632 -0.513291,1.50019 -0.156105,0.92075 0,2.22779 0.36777,3.20146 0.05821,0.15081 0.275167,0.127 0.29898,-0.0317 0.3175,-2.09286 1.021291,-3.66448 1.021291,-3.66448"
|
|
||||||
style="fill:#e2a610;stroke-width:0.264583"
|
|
||||||
id="path2-6-6" />
|
|
||||||
<path
|
|
||||||
d="m 90.540009,114.68528 c 0,0 -8.252354,0.89164 -11.975042,8.05656 -3.722687,7.16492 -0.558271,11.50937 2.791354,13.09158 3.349626,1.58221 11.789834,2.14048 17.279938,0.83873 5.490101,-1.30175 6.863291,-4.0005 6.606641,-6.60664 -0.37306,-3.80471 -3.90789,-6.14363 -3.90789,-6.14363 0,0 0.14023,-4.699 -3.50573,-7.74964 -3.235854,-2.70934 -7.289271,-1.48696 -7.289271,-1.48696"
|
|
||||||
style="fill:#ffca28;stroke-width:0.264583"
|
|
||||||
id="path3-0-1" />
|
|
||||||
<path
|
|
||||||
d="m 95.754947,113.04486 c 2.233083,0.7276 2.727854,2.74638 2.566458,3.29671 -0.193146,0.64558 -2.667,-1.86796 -6.344708,-1.71715 -1.285875,0.0529 -0.912813,-0.73554 -0.3175,-1.19062 0.785812,-0.60061 2.106083,-1.03452 4.09575,-0.38894"
|
|
||||||
style="fill:#6d4c41;stroke-width:0.264583"
|
|
||||||
id="path6-6-2" />
|
|
||||||
<path
|
|
||||||
d="m 95.754947,113.04486 c 2.233083,0.7276 2.727854,2.74638 2.566458,3.29671 -0.193146,0.64558 -2.667,-1.86796 -6.344708,-1.71715 -1.285875,0.0529 -0.912813,-0.73554 -0.3175,-1.19062 0.785812,-0.60061 2.106083,-1.03452 4.09575,-0.38894"
|
|
||||||
style="fill:#6b4b46;stroke-width:0.264583"
|
|
||||||
id="path7-2-9" />
|
|
||||||
<path
|
|
||||||
d="m 99.676072,120.16215 c 0.280458,-0.19314 1.222378,0.14023 1.486958,1.98438 0.12965,0.90223 0.16933,1.77535 0.16933,1.77535 0,0 -1.11125,-0.99748 -1.479017,-1.69862 -0.463021,-0.88636 -0.642938,-1.74361 -0.177271,-2.06111"
|
|
||||||
style="fill:#e2a610;stroke-width:0.264583"
|
|
||||||
id="path8-6-3" />
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:19.7556px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
|
|
||||||
x="125.45235"
|
|
||||||
y="92.696564"
|
|
||||||
id="text4-9-1"
|
|
||||||
transform="rotate(20.578693)"><tspan
|
|
||||||
id="tspan4-2-9"
|
|
||||||
style="font-size:19.7556px;fill:#4d4d4d;stroke-width:0.264583"
|
|
||||||
x="125.45235"
|
|
||||||
y="92.696564">$</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="layer2"
|
|
||||||
transform="translate(-1.4293676,48.496402)">
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:17.6389px;text-align:start;letter-spacing:-0.529167px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;stroke-width:0.264583"
|
|
||||||
x="57.635151"
|
|
||||||
y="55.655094"
|
|
||||||
id="text1"><tspan
|
|
||||||
id="tspan1"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:17.6389px;font-family:'Pirata One';-inkscape-font-specification:'Pirata One, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:-0.529167px;fill:#4d4d4d;stroke:none;stroke-width:0.264583"
|
|
||||||
x="57.635151"
|
|
||||||
y="55.655094">pendSparrow</tspan></text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.8 KiB |
@@ -1,7 +1,6 @@
|
|||||||
package test_test
|
package test_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"spend-sparrow/internal/db"
|
"spend-sparrow/internal/db"
|
||||||
"spend-sparrow/internal/types"
|
"spend-sparrow/internal/types"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -27,7 +26,7 @@ func setupDb(t *testing.T) *sqlx.DB {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
err = db.RunMigrations(context.Background(), d, "../")
|
err = db.RunMigrations(d, "../")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error running migrations: %v", err)
|
t.Fatalf("Error running migrations: %v", err)
|
||||||
}
|
}
|
||||||
@@ -48,14 +47,14 @@ func TestUser(t *testing.T) {
|
|||||||
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
|
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
|
||||||
expected := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
|
expected := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
|
||||||
|
|
||||||
err := underTest.InsertUser(context.Background(), expected)
|
err := underTest.InsertUser(expected)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
actual, err := underTest.GetUser(context.Background(), expected.Id)
|
actual, err := underTest.GetUser(expected.Id)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, expected, actual)
|
assert.Equal(t, expected, actual)
|
||||||
|
|
||||||
actual, err = underTest.GetUserByEmail(context.Background(), expected.Email)
|
actual, err = underTest.GetUserByEmail(expected.Email)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, expected, actual)
|
assert.Equal(t, expected, actual)
|
||||||
})
|
})
|
||||||
@@ -65,7 +64,7 @@ func TestUser(t *testing.T) {
|
|||||||
|
|
||||||
underTest := db.NewAuthSqlite(d)
|
underTest := db.NewAuthSqlite(d)
|
||||||
|
|
||||||
_, err := underTest.GetUserByEmail(context.Background(), "nonExistentEmail")
|
_, err := underTest.GetUserByEmail("nonExistentEmail")
|
||||||
assert.Equal(t, db.ErrNotFound, err)
|
assert.Equal(t, db.ErrNotFound, err)
|
||||||
})
|
})
|
||||||
t.Run("should return ErrUserExist", func(t *testing.T) {
|
t.Run("should return ErrUserExist", func(t *testing.T) {
|
||||||
@@ -78,10 +77,10 @@ func TestUser(t *testing.T) {
|
|||||||
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
|
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
|
||||||
user := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
|
user := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
|
||||||
|
|
||||||
err := underTest.InsertUser(context.Background(), user)
|
err := underTest.InsertUser(user)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = underTest.InsertUser(context.Background(), user)
|
err = underTest.InsertUser(user)
|
||||||
assert.Equal(t, db.ErrAlreadyExists, err)
|
assert.Equal(t, db.ErrAlreadyExists, err)
|
||||||
})
|
})
|
||||||
t.Run("should return ErrInternal on missing NOT NULL fields", func(t *testing.T) {
|
t.Run("should return ErrInternal on missing NOT NULL fields", func(t *testing.T) {
|
||||||
@@ -93,7 +92,7 @@ func TestUser(t *testing.T) {
|
|||||||
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
|
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
|
||||||
user := types.NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt)
|
user := types.NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt)
|
||||||
|
|
||||||
err := underTest.InsertUser(context.Background(), user)
|
err := underTest.InsertUser(user)
|
||||||
assert.Equal(t, types.ErrInternal, err)
|
assert.Equal(t, types.ErrInternal, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -111,21 +110,21 @@ func TestToken(t *testing.T) {
|
|||||||
expiresAt := createAt.Add(24 * time.Hour)
|
expiresAt := createAt.Add(24 * time.Hour)
|
||||||
expected := types.NewToken(uuid.New(), "sessionId", "token", types.TokenTypeCsrf, createAt, expiresAt)
|
expected := types.NewToken(uuid.New(), "sessionId", "token", types.TokenTypeCsrf, createAt, expiresAt)
|
||||||
|
|
||||||
err := underTest.InsertToken(context.Background(), expected)
|
err := underTest.InsertToken(expected)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
actual, err := underTest.GetToken(context.Background(), expected.Token)
|
actual, err := underTest.GetToken(expected.Token)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, expected, actual)
|
assert.Equal(t, expected, actual)
|
||||||
|
|
||||||
expected.SessionId = ""
|
expected.SessionId = ""
|
||||||
actuals, err := underTest.GetTokensByUserIdAndType(context.Background(), expected.UserId, expected.Type)
|
actuals, err := underTest.GetTokensByUserIdAndType(expected.UserId, expected.Type)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []*types.Token{expected}, actuals)
|
assert.Equal(t, []*types.Token{expected}, actuals)
|
||||||
|
|
||||||
expected.SessionId = "sessionId"
|
expected.SessionId = "sessionId"
|
||||||
expected.UserId = uuid.Nil
|
expected.UserId = uuid.Nil
|
||||||
actuals, err = underTest.GetTokensBySessionIdAndType(context.Background(), expected.SessionId, expected.Type)
|
actuals, err = underTest.GetTokensBySessionIdAndType(expected.SessionId, expected.Type)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []*types.Token{expected}, actuals)
|
assert.Equal(t, []*types.Token{expected}, actuals)
|
||||||
})
|
})
|
||||||
@@ -141,14 +140,14 @@ func TestToken(t *testing.T) {
|
|||||||
expected1 := types.NewToken(userId, "sessionId", "token1", types.TokenTypeCsrf, createAt, expiresAt)
|
expected1 := types.NewToken(userId, "sessionId", "token1", types.TokenTypeCsrf, createAt, expiresAt)
|
||||||
expected2 := types.NewToken(userId, "sessionId", "token2", types.TokenTypeCsrf, createAt, expiresAt)
|
expected2 := types.NewToken(userId, "sessionId", "token2", types.TokenTypeCsrf, createAt, expiresAt)
|
||||||
|
|
||||||
err := underTest.InsertToken(context.Background(), expected1)
|
err := underTest.InsertToken(expected1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = underTest.InsertToken(context.Background(), expected2)
|
err = underTest.InsertToken(expected2)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
expected1.UserId = uuid.Nil
|
expected1.UserId = uuid.Nil
|
||||||
expected2.UserId = uuid.Nil
|
expected2.UserId = uuid.Nil
|
||||||
actuals, err := underTest.GetTokensBySessionIdAndType(context.Background(), expected1.SessionId, expected1.Type)
|
actuals, err := underTest.GetTokensBySessionIdAndType(expected1.SessionId, expected1.Type)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []*types.Token{expected1, expected2}, actuals)
|
assert.Equal(t, []*types.Token{expected1, expected2}, actuals)
|
||||||
|
|
||||||
@@ -156,7 +155,7 @@ func TestToken(t *testing.T) {
|
|||||||
expected2.SessionId = ""
|
expected2.SessionId = ""
|
||||||
expected1.UserId = userId
|
expected1.UserId = userId
|
||||||
expected2.UserId = userId
|
expected2.UserId = userId
|
||||||
actuals, err = underTest.GetTokensByUserIdAndType(context.Background(), userId, expected1.Type)
|
actuals, err = underTest.GetTokensByUserIdAndType(userId, expected1.Type)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []*types.Token{expected1, expected2}, actuals)
|
assert.Equal(t, []*types.Token{expected1, expected2}, actuals)
|
||||||
})
|
})
|
||||||
@@ -166,13 +165,13 @@ func TestToken(t *testing.T) {
|
|||||||
|
|
||||||
underTest := db.NewAuthSqlite(d)
|
underTest := db.NewAuthSqlite(d)
|
||||||
|
|
||||||
_, err := underTest.GetToken(context.Background(), "nonExistent")
|
_, err := underTest.GetToken("nonExistent")
|
||||||
assert.Equal(t, db.ErrNotFound, err)
|
assert.Equal(t, db.ErrNotFound, err)
|
||||||
|
|
||||||
_, err = underTest.GetTokensByUserIdAndType(context.Background(), uuid.New(), types.TokenTypeEmailVerify)
|
_, err = underTest.GetTokensByUserIdAndType(uuid.New(), types.TokenTypeEmailVerify)
|
||||||
assert.Equal(t, db.ErrNotFound, err)
|
assert.Equal(t, db.ErrNotFound, err)
|
||||||
|
|
||||||
_, err = underTest.GetTokensBySessionIdAndType(context.Background(), "sessionId", types.TokenTypeEmailVerify)
|
_, err = underTest.GetTokensBySessionIdAndType("sessionId", types.TokenTypeEmailVerify)
|
||||||
assert.Equal(t, db.ErrNotFound, err)
|
assert.Equal(t, db.ErrNotFound, err)
|
||||||
})
|
})
|
||||||
t.Run("should return ErrAlreadyExists", func(t *testing.T) {
|
t.Run("should return ErrAlreadyExists", func(t *testing.T) {
|
||||||
@@ -185,10 +184,10 @@ func TestToken(t *testing.T) {
|
|||||||
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
|
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
|
||||||
user := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
|
user := types.NewUser(uuid.New(), "some@email.de", true, &verifiedAt, false, []byte("somePass"), []byte("someSalt"), createAt)
|
||||||
|
|
||||||
err := underTest.InsertUser(context.Background(), user)
|
err := underTest.InsertUser(user)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = underTest.InsertUser(context.Background(), user)
|
err = underTest.InsertUser(user)
|
||||||
assert.Equal(t, db.ErrAlreadyExists, err)
|
assert.Equal(t, db.ErrAlreadyExists, err)
|
||||||
})
|
})
|
||||||
t.Run("should return ErrInternal on missing NOT NULL fields", func(t *testing.T) {
|
t.Run("should return ErrInternal on missing NOT NULL fields", func(t *testing.T) {
|
||||||
@@ -200,7 +199,7 @@ func TestToken(t *testing.T) {
|
|||||||
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
|
createAt := time.Date(2020, 1, 5, 12, 0, 0, 0, time.UTC)
|
||||||
user := types.NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt)
|
user := types.NewUser(uuid.New(), "some@email.de", false, nil, false, []byte("somePass"), nil, createAt)
|
||||||
|
|
||||||
err := underTest.InsertUser(context.Background(), user)
|
err := underTest.InsertUser(user)
|
||||||
assert.Equal(t, types.ErrInternal, err)
|
assert.Equal(t, types.ErrInternal, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package test_test
|
package test_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"spend-sparrow/internal/db"
|
"spend-sparrow/internal/db"
|
||||||
"spend-sparrow/internal/service"
|
"spend-sparrow/internal/service"
|
||||||
"spend-sparrow/internal/types"
|
"spend-sparrow/internal/types"
|
||||||
@@ -37,7 +36,7 @@ func TestSignUp(t *testing.T) {
|
|||||||
|
|
||||||
underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings)
|
underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings)
|
||||||
|
|
||||||
_, err := underTest.SignUp(context.Background(), "invalid email address", "SomeStrongPassword123!")
|
_, err := underTest.SignUp("invalid email address", "SomeStrongPassword123!")
|
||||||
|
|
||||||
assert.Equal(t, service.ErrInvalidEmail, err)
|
assert.Equal(t, service.ErrInvalidEmail, err)
|
||||||
})
|
})
|
||||||
@@ -59,7 +58,7 @@ func TestSignUp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, password := range weakPasswords {
|
for _, password := range weakPasswords {
|
||||||
_, err := underTest.SignUp(context.Background(), "some@valid.email", password)
|
_, err := underTest.SignUp("some@valid.email", password)
|
||||||
assert.Equal(t, service.ErrInvalidPassword, err)
|
assert.Equal(t, service.ErrInvalidPassword, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -79,15 +78,13 @@ func TestSignUp(t *testing.T) {
|
|||||||
|
|
||||||
expected := types.NewUser(userId, email, false, nil, false, service.GetHashPassword(password, salt), salt, createTime)
|
expected := types.NewUser(userId, email, false, nil, false, service.GetHashPassword(password, salt), salt, createTime)
|
||||||
|
|
||||||
ctx := context.Background()
|
mockRandom.EXPECT().UUID().Return(userId, nil)
|
||||||
|
mockRandom.EXPECT().Bytes(16).Return(salt, nil)
|
||||||
mockRandom.EXPECT().UUID(ctx).Return(userId, nil)
|
|
||||||
mockRandom.EXPECT().Bytes(ctx, 16).Return(salt, nil)
|
|
||||||
mockClock.EXPECT().Now().Return(createTime)
|
mockClock.EXPECT().Now().Return(createTime)
|
||||||
mockAuthDb.EXPECT().InsertUser(context.Background(), expected).Return(nil)
|
mockAuthDb.EXPECT().InsertUser(expected).Return(nil)
|
||||||
|
|
||||||
underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings)
|
underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings)
|
||||||
actual, err := underTest.SignUp(context.Background(), email, password)
|
actual, err := underTest.SignUp(email, password)
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -108,16 +105,15 @@ func TestSignUp(t *testing.T) {
|
|||||||
salt := []byte("salt")
|
salt := []byte("salt")
|
||||||
user := types.NewUser(userId, email, false, nil, false, service.GetHashPassword(password, salt), salt, createTime)
|
user := types.NewUser(userId, email, false, nil, false, service.GetHashPassword(password, salt), salt, createTime)
|
||||||
|
|
||||||
ctx := context.Background()
|
mockRandom.EXPECT().UUID().Return(user.Id, nil)
|
||||||
mockRandom.EXPECT().UUID(ctx).Return(user.Id, nil)
|
mockRandom.EXPECT().Bytes(16).Return(salt, nil)
|
||||||
mockRandom.EXPECT().Bytes(ctx, 16).Return(salt, nil)
|
|
||||||
mockClock.EXPECT().Now().Return(createTime)
|
mockClock.EXPECT().Now().Return(createTime)
|
||||||
|
|
||||||
mockAuthDb.EXPECT().InsertUser(context.Background(), user).Return(db.ErrAlreadyExists)
|
mockAuthDb.EXPECT().InsertUser(user).Return(db.ErrAlreadyExists)
|
||||||
|
|
||||||
underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings)
|
underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings)
|
||||||
|
|
||||||
_, err := underTest.SignUp(context.Background(), user.Email, password)
|
_, err := underTest.SignUp(user.Email, password)
|
||||||
assert.Equal(t, service.ErrAccountExists, err)
|
assert.Equal(t, service.ErrAccountExists, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -144,14 +140,14 @@ func TestSendVerificationMail(t *testing.T) {
|
|||||||
mockClock := mocks.NewMockClock(t)
|
mockClock := mocks.NewMockClock(t)
|
||||||
mockMail := mocks.NewMockMail(t)
|
mockMail := mocks.NewMockMail(t)
|
||||||
|
|
||||||
ctx := context.Background()
|
mockAuthDb.EXPECT().GetTokensByUserIdAndType(userId, types.TokenTypeEmailVerify).Return(tokens, nil)
|
||||||
mockAuthDb.EXPECT().GetTokensByUserIdAndType(context.Background(), userId, types.TokenTypeEmailVerify).Return(tokens, nil)
|
|
||||||
mockMail.EXPECT().SendMail(ctx, email, "Welcome to spend-sparrow", mock.MatchedBy(func(message string) bool {
|
mockMail.EXPECT().SendMail(email, "Welcome to spend-sparrow", mock.MatchedBy(func(message string) bool {
|
||||||
return strings.Contains(message, token.Token)
|
return strings.Contains(message, token.Token)
|
||||||
})).Return()
|
})).Return()
|
||||||
|
|
||||||
underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings)
|
underTest := service.NewAuth(mockAuthDb, mockRandom, mockClock, mockMail, &settings)
|
||||||
|
|
||||||
underTest.SendVerificationMail(context.Background(), userId, email)
|
underTest.SendVerificationMail(userId, email)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ func getTokenAttribute(t *testing.T, data *html.Node) string {
|
|||||||
|
|
||||||
for _, attr := range data.Attr {
|
for _, attr := range data.Attr {
|
||||||
if attr.Key == "hx-headers" {
|
if attr.Key == "hx-headers" {
|
||||||
var data map[string]any
|
var data map[string]interface{}
|
||||||
err := json.Unmarshal([]byte(attr.Val), &data)
|
err := json.Unmarshal([]byte(attr.Val), &data)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
result, ok := data["Csrf-Token"].(string)
|
result, ok := data["Csrf-Token"].(string)
|
||||||
@@ -182,16 +182,16 @@ func createValidUserSession(t *testing.T, db *sqlx.DB, add string) (uuid.UUID, s
|
|||||||
csrfToken := "my-verifying-token" + add
|
csrfToken := "my-verifying-token" + add
|
||||||
email := add + "mail@mail.de"
|
email := add + "mail@mail.de"
|
||||||
|
|
||||||
_, err := db.ExecContext(context.Background(), `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, ?, TRUE, FALSE, ?, ?, datetime())`, userId, email, pass, []byte("salt"))
|
VALUES (?, ?, TRUE, FALSE, ?, ?, datetime())`, userId, email, pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.ExecContext(context.Background(), `
|
_, err = db.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = db.ExecContext(context.Background(), `
|
_, err = db.Exec(`
|
||||||
INSERT INTO token (token, user_id, session_id, type, created_at, expires_at)
|
INSERT INTO token (token, user_id, session_id, type, created_at, expires_at)
|
||||||
VALUES (?, ?, ?, ?, datetime(), datetime("now", "+1 day"))`, csrfToken, userId, sessionId, types.TokenTypeCsrf)
|
VALUES (?, ?, ?, ?, datetime(), datetime("now", "+1 day"))`, csrfToken, userId, sessionId, types.TokenTypeCsrf)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -112,11 +112,11 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
sessionId := "session-id"
|
sessionId := "session-id"
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
|
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.ExecContext(ctx, `
|
_, err = db.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -138,7 +138,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
|
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -165,7 +165,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
|
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -208,7 +208,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
|
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -248,7 +248,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
db, basePath, ctx := setupIntegrationTest(t)
|
db, basePath, ctx := setupIntegrationTest(t)
|
||||||
|
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, uuid.New(), pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, uuid.New(), pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -296,7 +296,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
db, basePath, ctx := setupIntegrationTest(t)
|
db, basePath, ctx := setupIntegrationTest(t)
|
||||||
|
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, uuid.New(), pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, uuid.New(), pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -415,7 +415,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
db, basePath, ctx := setupIntegrationTest(t)
|
db, basePath, ctx := setupIntegrationTest(t)
|
||||||
|
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, uuid.New(), pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, uuid.New(), pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -451,10 +451,10 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM session WHERE session_id = ?", anonymousSession.Value).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM session WHERE session_id = ?", anonymousSession.Value).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 0, rows)
|
assert.Equal(t, 0, rows)
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM token WHERE token = ?", anonymousCsrfToken).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM token WHERE token = ?", anonymousCsrfToken).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 0, rows)
|
assert.Equal(t, 0, rows)
|
||||||
})
|
})
|
||||||
@@ -469,11 +469,11 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
sessionId := "session-id"
|
sessionId := "session-id"
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
|
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.ExecContext(ctx, `
|
_, err = db.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -548,7 +548,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
|
|
||||||
db, basePath, ctx := setupIntegrationTest(t)
|
db, basePath, ctx := setupIntegrationTest(t)
|
||||||
|
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, uuid.New(), service.GetHashPassword("password", []byte("salt")), []byte("salt"))
|
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, uuid.New(), service.GetHashPassword("password", []byte("salt")), []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -627,11 +627,11 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Contains(t, resp.Header.Get("Hx-Trigger"), "An activation link has been send to your email")
|
assert.Contains(t, resp.Header.Get("Hx-Trigger"), "An activation link has been send to your email")
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE email = ? AND email_verified = FALSE", "mail@mail.de").Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE email = ? AND email_verified = FALSE", "mail@mail.de").Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, rows)
|
assert.Equal(t, 1, rows)
|
||||||
var token string
|
var token string
|
||||||
err = db.QueryRowContext(ctx, "SELECT t.token FROM token t INNER JOIN user u ON u.user_id = t.user_id WHERE u.email = ? AND t.type = ?", "mail@mail.de", types.TokenTypeEmailVerify).Scan(&token)
|
err = db.QueryRow("SELECT t.token FROM token t INNER JOIN user u ON u.user_id = t.user_id WHERE u.email = ? AND t.type = ?", "mail@mail.de", types.TokenTypeEmailVerify).Scan(&token)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, token)
|
assert.NotEmpty(t, token)
|
||||||
})
|
})
|
||||||
@@ -644,7 +644,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
|
|
||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
|
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -658,7 +658,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = FALSE", userId).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = FALSE", userId).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, rows)
|
assert.Equal(t, 1, rows)
|
||||||
})
|
})
|
||||||
@@ -670,11 +670,11 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
token := "my-outdated-verifying-token"
|
token := "my-outdated-verifying-token"
|
||||||
|
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.ExecContext(ctx, `
|
_, err = db.Exec(`
|
||||||
INSERT INTO token (token, user_id, type, created_at, expires_at)
|
INSERT INTO token (token, user_id, type, created_at, expires_at)
|
||||||
VALUES (?, ?, ?, datetime("now", "-16 minute"), datetime("now", "-1 minute"))`, token, userId, types.TokenTypeEmailVerify)
|
VALUES (?, ?, ?, datetime("now", "-16 minute"), datetime("now", "-1 minute"))`, token, userId, types.TokenTypeEmailVerify)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -688,7 +688,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = FALSE", userId).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = FALSE", userId).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, rows)
|
assert.Equal(t, 1, rows)
|
||||||
})
|
})
|
||||||
@@ -700,11 +700,11 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
token := "my-verifying-token"
|
token := "my-verifying-token"
|
||||||
|
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.ExecContext(ctx, `
|
_, err = db.Exec(`
|
||||||
INSERT INTO token (token, user_id, session_id, type, created_at, expires_at)
|
INSERT INTO token (token, user_id, session_id, type, created_at, expires_at)
|
||||||
VALUES (?, ?, "", ?, datetime("now"), datetime("now", "+15 minute"))`, token, userId, types.TokenTypeEmailVerify)
|
VALUES (?, ?, "", ?, datetime("now"), datetime("now", "+15 minute"))`, token, userId, types.TokenTypeEmailVerify)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -718,7 +718,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = TRUE", userId).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND email_verified = TRUE", userId).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, rows)
|
assert.Equal(t, 1, rows)
|
||||||
})
|
})
|
||||||
@@ -747,16 +747,16 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
sessionId := "session-id"
|
sessionId := "session-id"
|
||||||
|
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.ExecContext(ctx, `
|
_, err = db.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/dashboard", nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, basePath+"/", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Cookie", "id="+sessionId)
|
req.Header.Set("Cookie", "id="+sessionId)
|
||||||
resp, err := httpClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
@@ -765,7 +765,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
var csrfToken string
|
var csrfToken string
|
||||||
err = db.QueryRowContext(ctx, "SELECT token FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypeCsrf).Scan(&csrfToken)
|
err = db.QueryRow("SELECT token FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypeCsrf).Scan(&csrfToken)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signout", nil)
|
req, err = http.NewRequestWithContext(ctx, http.MethodPost, basePath+"/api/auth/signout", nil)
|
||||||
@@ -785,7 +785,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, -1, cookie.MaxAge)
|
assert.Equal(t, -1, cookie.MaxAge)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 0, rows)
|
assert.Equal(t, 0, rows)
|
||||||
})
|
})
|
||||||
@@ -825,13 +825,13 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
|
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
|
|
||||||
sessionId := "session-id"
|
sessionId := "session-id"
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.ExecContext(ctx, `
|
_, err = db.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -871,13 +871,13 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
|
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
|
|
||||||
sessionId := "session-id"
|
sessionId := "session-id"
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.ExecContext(ctx, `
|
_, err = db.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -964,22 +964,22 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 0, rows)
|
assert.Equal(t, 0, rows)
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM token WHERE user_id = ?", userId).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM token WHERE user_id = ?", userId).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 0, rows)
|
assert.Equal(t, 0, rows)
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ?", userId).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ?", userId).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 0, rows)
|
assert.Equal(t, 0, rows)
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM account WHERE user_id = ?", userId).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM account WHERE user_id = ?", userId).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 0, rows)
|
assert.Equal(t, 0, rows)
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM treasure_chest WHERE user_id = ?", userId).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM treasure_chest WHERE user_id = ?", userId).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 0, rows)
|
assert.Equal(t, 0, rows)
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM \"transaction\" WHERE user_id = ?", userId).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM \"transaction\" WHERE user_id = ?", userId).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 0, rows)
|
assert.Equal(t, 0, rows)
|
||||||
})
|
})
|
||||||
@@ -1040,13 +1040,13 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
|
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
|
|
||||||
sessionId := "session-id"
|
sessionId := "session-id"
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.ExecContext(ctx, `
|
_, err = db.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1069,7 +1069,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, rows)
|
assert.Equal(t, 1, rows)
|
||||||
})
|
})
|
||||||
@@ -1080,13 +1080,13 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
|
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
|
|
||||||
sessionId := "session-id"
|
sessionId := "session-id"
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.ExecContext(ctx, `
|
_, err = db.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1119,7 +1119,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, rows)
|
assert.Equal(t, 1, rows)
|
||||||
})
|
})
|
||||||
@@ -1130,13 +1130,13 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
|
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
|
|
||||||
sessionId := "session-id"
|
sessionId := "session-id"
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.ExecContext(ctx, `
|
_, err = db.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1169,7 +1169,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, rows)
|
assert.Equal(t, 1, rows)
|
||||||
})
|
})
|
||||||
@@ -1181,21 +1181,21 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userIdOther := uuid.New()
|
userIdOther := uuid.New()
|
||||||
|
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
|
|
||||||
sessionId := "session-id"
|
sessionId := "session-id"
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.ExecContext(ctx, `
|
_, err = db.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.ExecContext(ctx, `
|
_, err = db.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES ("second", ?, datetime(), datetime("now", "+1 day"))`, userId)
|
VALUES ("second", ?, datetime(), datetime("now", "+1 day"))`, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.ExecContext(ctx, `
|
_, err = db.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES ("other", ?, datetime(), datetime("now", "+1 day"))`, userIdOther)
|
VALUES ("other", ?, datetime(), datetime("now", "+1 day"))`, userIdOther)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1232,12 +1232,12 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
|
|
||||||
pass = service.GetHashPassword("MyNewSecurePassword1!", []byte("salt"))
|
pass = service.GetHashPassword("MyNewSecurePassword1!", []byte("salt"))
|
||||||
var rows int
|
var rows int
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, rows)
|
assert.Equal(t, 1, rows)
|
||||||
|
|
||||||
var sessionIds []string
|
var sessionIds []string
|
||||||
sessions, err := db.QueryContext(ctx, `SELECT session_id FROM session WHERE NOT user_id = ? ORDER BY session_id`, uuid.Nil)
|
sessions, err := db.Query(`SELECT session_id FROM session WHERE NOT user_id = ? ORDER BY session_id`, uuid.Nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
for sessions.Next() {
|
for sessions.Next() {
|
||||||
var sessionId string
|
var sessionId string
|
||||||
@@ -1260,13 +1260,13 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
|
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := d.ExecContext(ctx, `
|
_, err := d.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
sessionId := "session-id"
|
sessionId := "session-id"
|
||||||
_, err = d.ExecContext(ctx, `
|
_, err = d.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES ("session-id", ?, datetime(), datetime("now", "+1 day"))`, userId)
|
VALUES ("session-id", ?, datetime(), datetime("now", "+1 day"))`, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1288,7 +1288,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
|
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := d.ExecContext(ctx, `
|
_, err := d.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1317,7 +1317,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = d.QueryRowContext(ctx, "SELECT COUNT(*) FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypePasswordReset).Scan(&rows)
|
err = d.QueryRow("SELECT COUNT(*) FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypePasswordReset).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 0, rows)
|
assert.Equal(t, 0, rows)
|
||||||
})
|
})
|
||||||
@@ -1363,7 +1363,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
|
|
||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1399,7 +1399,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Contains(t, resp.Header.Get("Hx-Trigger"), msg)
|
assert.Contains(t, resp.Header.Get("Hx-Trigger"), msg)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypePasswordReset).Scan(&rows)
|
err = db.QueryRow("SELECT COUNT(*) FROM token WHERE user_id = ? AND type = ?", userId, types.TokenTypePasswordReset).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, rows)
|
assert.Equal(t, 1, rows)
|
||||||
})
|
})
|
||||||
@@ -1413,7 +1413,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
|
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := d.ExecContext(ctx, `
|
_, err := d.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1445,7 +1445,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = d.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
|
err = d.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, rows)
|
assert.Equal(t, 1, rows)
|
||||||
})
|
})
|
||||||
@@ -1456,7 +1456,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
|
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := d.ExecContext(ctx, `
|
_, err := d.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1473,7 +1473,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.NotEmpty(t, anonymousCsrfToken)
|
assert.NotEmpty(t, anonymousCsrfToken)
|
||||||
|
|
||||||
token := "password-reset-token"
|
token := "password-reset-token"
|
||||||
_, err = d.ExecContext(ctx, `
|
_, err = d.Exec(`
|
||||||
INSERT INTO token (token, user_id, session_id, type, created_at, expires_at)
|
INSERT INTO token (token, user_id, session_id, type, created_at, expires_at)
|
||||||
VALUES (?, ?, ?, ?, datetime("now", "-16 minute"), datetime("now", "-1 minute"))`, token, userId, "", types.TokenTypePasswordReset)
|
VALUES (?, ?, ?, ?, datetime("now", "-16 minute"), datetime("now", "-1 minute"))`, token, userId, "", types.TokenTypePasswordReset)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1494,7 +1494,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = d.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
|
err = d.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, rows)
|
assert.Equal(t, 1, rows)
|
||||||
})
|
})
|
||||||
@@ -1505,7 +1505,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
|
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := d.ExecContext(ctx, `
|
_, err := d.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1522,7 +1522,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.NotEmpty(t, anonymousCsrfToken)
|
assert.NotEmpty(t, anonymousCsrfToken)
|
||||||
|
|
||||||
token := "password-reset-token"
|
token := "password-reset-token"
|
||||||
_, err = d.ExecContext(ctx, `
|
_, err = d.Exec(`
|
||||||
INSERT INTO token (token, user_id, session_id, type, created_at, expires_at)
|
INSERT INTO token (token, user_id, session_id, type, created_at, expires_at)
|
||||||
VALUES (?, ?, ?, ?, datetime("now"), datetime("now", "+15 minute"))`, token, userId, "", types.TokenTypePasswordReset)
|
VALUES (?, ?, ?, ?, datetime("now"), datetime("now", "+15 minute"))`, token, userId, "", types.TokenTypePasswordReset)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1543,7 +1543,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = d.QueryRowContext(ctx, "SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
|
err = d.QueryRow("SELECT COUNT(*) FROM user WHERE user_id = ? AND password = ?", userId, pass).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, rows)
|
assert.Equal(t, 1, rows)
|
||||||
})
|
})
|
||||||
@@ -1554,12 +1554,12 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
|
|
||||||
pass := service.GetHashPassword("password", []byte("salt"))
|
pass := service.GetHashPassword("password", []byte("salt"))
|
||||||
_, err := d.ExecContext(ctx, `
|
_, err := d.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt"))
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = d.ExecContext(ctx, `
|
_, err = d.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES ("session-id", ?, datetime(), datetime("now", "+1 day"))`, userId)
|
VALUES ("session-id", ?, datetime(), datetime("now", "+1 day"))`, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1590,7 +1590,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
var token string
|
var token string
|
||||||
err = d.QueryRowContext(ctx, "SELECT token FROM token WHERE type = ?", types.TokenTypePasswordReset).Scan(&token)
|
err = d.QueryRow("SELECT token FROM token WHERE type = ?", types.TokenTypePasswordReset).Scan(&token)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
formData = url.Values{
|
formData = url.Values{
|
||||||
@@ -1608,7 +1608,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
sessions, err := d.QueryContext(ctx, "SELECT session_id FROM session WHERE user_id = ?", userId)
|
sessions, err := d.Query("SELECT session_id FROM session WHERE user_id = ?", userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, sessions.Next())
|
assert.False(t, sessions.Next())
|
||||||
})
|
})
|
||||||
@@ -1623,11 +1623,11 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
sessionId := "session-id"
|
sessionId := "session-id"
|
||||||
|
|
||||||
_, err := d.ExecContext(ctx, `
|
_, err := d.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = d.ExecContext(ctx, `
|
_, err = d.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES (?, ?, datetime("now", "-8 hour"), datetime("now", "-1 minute"))`, sessionId, userId)
|
VALUES (?, ?, datetime("now", "-8 hour"), datetime("now", "-1 minute"))`, sessionId, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1643,7 +1643,7 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
assert.NotEqual(t, sessionId, newSession.Value)
|
assert.NotEqual(t, sessionId, newSession.Value)
|
||||||
|
|
||||||
var rows int
|
var rows int
|
||||||
err = d.QueryRowContext(ctx, "SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows)
|
err = d.QueryRow("SELECT COUNT(*) FROM session WHERE user_id = ?", userId).Scan(&rows)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 0, rows)
|
assert.Equal(t, 0, rows)
|
||||||
})
|
})
|
||||||
@@ -1670,11 +1670,11 @@ func TestIntegrationAuth(t *testing.T) {
|
|||||||
userId := uuid.New()
|
userId := uuid.New()
|
||||||
sessionId := "session-id"
|
sessionId := "session-id"
|
||||||
|
|
||||||
_, err := d.ExecContext(ctx, `
|
_, err := d.Exec(`
|
||||||
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at)
|
||||||
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
|
VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = d.ExecContext(ctx, `
|
_, err = d.Exec(`
|
||||||
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
INSERT INTO session (session_id, user_id, created_at, expires_at)
|
||||||
VALUES (?, ?, datetime("now", "-8 hour"), datetime("now", "-1 minute"))`, sessionId, userId)
|
VALUES (?, ?, datetime("now", "-8 hour"), datetime("now", "-1 minute"))`, sessionId, userId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1769,7 +1769,7 @@ func TestIntegrationAccount(t *testing.T) {
|
|||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
var id uuid.UUID
|
var id uuid.UUID
|
||||||
err = db.GetContext(ctx, &id, "SELECT id FROM account")
|
err = db.Get(&id, "SELECT id FROM account")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
@@ -1862,6 +1862,7 @@ func TestIntegrationAccount(t *testing.T) {
|
|||||||
">": 400,
|
">": 400,
|
||||||
"/": 400,
|
"/": 400,
|
||||||
"\\": 400,
|
"\\": 400,
|
||||||
|
"?": 400,
|
||||||
":": 400,
|
":": 400,
|
||||||
"*": 400,
|
"*": 400,
|
||||||
"|": 400,
|
"|": 400,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func TestTreasureChestShouldNotDeleteIfTransactionRecurringExists(t *testing.T)
|
|||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
var parentId string
|
var parentId string
|
||||||
err := db.GetContext(ctx, &parentId, "SELECT id FROM treasure_chest")
|
err := db.Get(&parentId, "SELECT id FROM treasure_chest")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
formData = url.Values{
|
formData = url.Values{
|
||||||
@@ -33,7 +33,7 @@ func TestTreasureChestShouldNotDeleteIfTransactionRecurringExists(t *testing.T)
|
|||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
var childId string
|
var childId string
|
||||||
err = db.GetContext(ctx, &childId, "SELECT id FROM treasure_chest WHERE parent_id = ?", parentId)
|
err = db.Get(&childId, "SELECT id FROM treasure_chest WHERE parent_id = ?", parentId)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
formData = url.Values{
|
formData = url.Values{
|
||||||
|
|||||||
Reference in New Issue
Block a user