Compare commits

..

15 commits

Author SHA1 Message Date
qpismont
2445b82395 Update golangci-lint version to 2.1.2 and adjust CI lint command for improved performance.
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/tests Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2025-04-17 21:21:04 +00:00
qpismont
f8f0f0eca2 Update VSCode settings and CI lint configuration: modify Go lint flags for improved performance and update golangci-lint version to 2.1.2.
Some checks failed
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline was successful
ci/woodpecker/push/build unknown status
2025-04-17 20:34:14 +00:00
qpismont
edc9aeb471 Remove FetchOneByRoleId method and its corresponding SQL constant from the account repository to streamline account management functionality.
Some checks failed
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline was successful
ci/woodpecker/push/build unknown status
2025-04-17 20:24:20 +00:00
qpismont
1f31d9c78e Add account registration feature: introduce AccountRegister type, implement Register method in AccountService, and add corresponding tests. Also, create variable management API with CRUD operations and database migration for variables table.
Some checks failed
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline failed
ci/woodpecker/push/build unknown status
2025-04-17 20:24:05 +00:00
qpismont
3f7fe22392 upgrade go 1.24.2
Some checks failed
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline was successful
ci/woodpecker/push/build unknown status
2025-04-08 18:50:30 +02:00
qpismont
3ea22fd39d Refactor devcontainer configuration: update network settings and container name for improved development environment consistency.
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/tests Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2025-03-31 19:46:54 +00:00
qpismont
dabfc43678 Add run arguments to devcontainer configuration: set network to 'dev-network' and name to 'trepa-dev'.
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/tests Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2025-03-27 22:08:45 +00:00
qpismont
923083cc5f Remove magiconair/properties dependency from go.mod and go.sum
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/tests Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2025-03-24 21:19:25 +00:00
qpismont
7c810ded85 Update dependencies and improve account tests: add objx library, replace assert library with testify, enhance password assertions, and modify SQL fixtures for password hashing.
Some checks failed
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/tests Pipeline failed
ci/woodpecker/push/build unknown status
2025-03-24 21:18:01 +00:00
qpismont
9d544c7e8a Update CI configuration to use Go 1.24.1-alpine for build, lint, and test steps. Fix test database setup path in account tests and improve error handling in JWT verification.
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/tests Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2025-03-24 20:55:54 +00:00
qpismont
a5e059a636 Add account management features: implement GetAccount endpoint, update Login method to return token, enhance repository and service layers, and add JWT middleware for authentication. Update .gitignore to include tmp directory.
Some checks failed
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline failed
ci/woodpecker/push/build unknown status
2025-03-24 20:43:00 +00:00
qpismont
c2a78d820e Update Go version to 1.24.1 in go.mod
Some checks failed
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline failed
ci/woodpecker/push/build unknown status
2025-03-18 21:20:03 +00:00
qpismont
3e8171162b Update dependencies, improve password handling, and enhance devcontainer configuration. Bump Go version to 1.24.1, add Air version to Dockerfile, and refactor password hashing and comparison functions to return errors. Update tests accordingly.
Some checks failed
ci/woodpecker/push/tests Pipeline is pending
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/build unknown status
2025-03-18 21:19:49 +00:00
qpismont
26ce8522ac Refactor account handling: update Login method signature, introduce AccountLogin and AccountCreate types, modify repository interface, and implement login logic in service. Add environment variable loading and hashing functions with tests.
Some checks failed
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/tests Pipeline failed
ci/woodpecker/push/build unknown status
2025-03-12 21:36:57 +00:00
qpismont
dcc5df8300 starting accounts 2025-03-11 21:42:36 +00:00
61 changed files with 999 additions and 173 deletions

51
.air.toml Normal file
View file

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

View file

@ -5,14 +5,17 @@ WORKDIR /app
ARG GO_VERSION ARG GO_VERSION
ARG GOLANGCI_LINT_VERSION ARG GOLANGCI_LINT_VERSION
ARG MIGRATE_VERSION ARG MIGRATE_VERSION
ARG AIR_VERSION
RUN apt update &&\ RUN apt update &&\
apt install git wget curl -y &&\ apt install git wget curl -y &&\
wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz &&\ wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz &&\
rm -rf /usr/local/go && tar -C /usr/local -xzf go$GO_VERSION.linux-amd64.tar.gz &&\ rm -rf /usr/local/go && tar -C /usr/local -xzf go$GO_VERSION.linux-amd64.tar.gz &&\
curl -sSfL https://raw.githubusercontent.com/air-verse/air/master/install.sh | sh -s -- -b /usr/local/go/bin &&\
wget https://github.com/golangci/golangci-lint/releases/download/v$GOLANGCI_LINT_VERSION/golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.deb &&\ wget https://github.com/golangci/golangci-lint/releases/download/v$GOLANGCI_LINT_VERSION/golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.deb &&\
dpkg -i golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.deb &&\ dpkg -i golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.deb &&\
wget https://github.com/golang-migrate/migrate/releases/download/v$MIGRATE_VERSION/migrate.linux-amd64.deb &&\ wget https://github.com/golang-migrate/migrate/releases/download/v$MIGRATE_VERSION/migrate.linux-amd64.deb &&\
dpkg -i migrate.linux-amd64.deb &&\ dpkg -i migrate.linux-amd64.deb &&\
wget https://github.com/air-verse/air/releases/download/v$AIR_VERSION/air_${AIR_VERSION}_linux_amd64 &&\
chmod +x air_${AIR_VERSION}_linux_amd64 &&\
mv air_${AIR_VERSION}_linux_amd64 /usr/local/go/bin/air &&\
echo "export PATH=$PATH:/usr/local/go/bin" > /root/.bashrc echo "export PATH=$PATH:/usr/local/go/bin" > /root/.bashrc

View file

@ -4,9 +4,10 @@
"build": { "build": {
"dockerfile": "Dockerfile", "dockerfile": "Dockerfile",
"args": { "args": {
"GO_VERSION": "1.24.0", "GO_VERSION": "1.24.2",
"GOLANGCI_LINT_VERSION": "1.64.5", "GOLANGCI_LINT_VERSION": "2.1.2",
"MIGRATE_VERSION": "4.18.2" "MIGRATE_VERSION": "4.18.2",
"AIR_VERSION": "1.61.7"
} }
}, },
"customizations": { "customizations": {
@ -16,6 +17,10 @@
] ]
} }
}, },
"runArgs": [
"--network=dev-network",
"--name=trepa-dev"
],
"forwardPorts": [ "forwardPorts": [
3000 3000
] ]

4
.gitignore vendored
View file

@ -25,4 +25,6 @@ go.work.sum
.env .env
# dist directory # dist directory
dist/ dist/
tmp/

11
.vscode/settings.json vendored
View file

@ -2,6 +2,15 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"go.lintTool": "golangci-lint", "go.lintTool": "golangci-lint",
"go.lintFlags": [ "go.lintFlags": [
"--fast" "--path-mode=abs",
"--fast-only"
],
"go.formatTool": "custom",
"go.alternateTools": {
"customFormatter": "golangci-lint"
},
"go.formatFlags": [
"fmt",
"--stdin"
] ]
} }

View file

@ -0,0 +1,3 @@
meta {
name: accounts
}

18
bruno/accounts/login.bru Normal file
View file

@ -0,0 +1,18 @@
meta {
name: login
type: http
seq: 1
}
post {
url: {{base_url}}/accounts/login
body: json
auth: inherit
}
body:json {
{
"username": "lol",
"password": "lol"
}
}

9
bruno/bruno.json Normal file
View file

@ -0,0 +1,9 @@
{
"version": "1",
"name": "trepa",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View file

@ -0,0 +1,3 @@
vars {
base_url: 127.0.0.1:3000
}

View file

@ -0,0 +1,3 @@
vars {
base_url: {{process.env.BASE_URL}}
}

View file

@ -3,7 +3,7 @@ when:
steps: steps:
- name: build - name: build
image: golang:1.24-alpine image: golang:1.24.2-alpine
commands: commands:
- apk update - apk update
- apk add bash - apk add bash

View file

@ -3,9 +3,9 @@ when:
steps: steps:
- name: lint - name: lint
image: golang:1.24-alpine image: golang:1.24.2-alpine
commands: commands:
- apk update - apk update
- apk add bash curl jq - apk add bash curl jq
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.64.5 - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.2
- $(go env GOPATH)/bin/golangci-lint run --fast - $(go env GOPATH)/bin/golangci-lint run --fast-only --path-mode=abs

View file

@ -3,7 +3,7 @@ when:
steps: steps:
- name: tests - name: tests
image: golang:1.24-alpine image: golang:1.24.2-alpine
environment: environment:
TEST_DB_HOST: db TEST_DB_HOST: db
TEST_DB_PORT: 5432 TEST_DB_PORT: 5432

View file

@ -11,7 +11,7 @@ import (
"syscall" "syscall"
"time" "time"
"gitea.qpismont.fr/qpismont/trepa/internal/accounts" "gitea.qpismont.fr/qpismont/trepa/internal/accounts/api"
"gitea.qpismont.fr/qpismont/trepa/internal/core" "gitea.qpismont.fr/qpismont/trepa/internal/core"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
@ -105,7 +105,7 @@ func setupDB() *sqlx.DB {
func setupRouter(db *sqlx.DB) *core.ServerMux { func setupRouter(db *sqlx.DB) *core.ServerMux {
router := core.NewServerMux() router := core.NewServerMux()
accounts.BindRoutes(router, db) api.BindRoutes(router, db)
return router return router
} }

18
go.mod
View file

@ -1,25 +1,33 @@
module gitea.qpismont.fr/qpismont/trepa module gitea.qpismont.fr/qpismont/trepa
go 1.24.0 go 1.24.2
require ( require (
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/go-playground/validator/v10 v10.26.0
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/jackc/pgx v3.6.2+incompatible github.com/jackc/pgx v3.6.2+incompatible
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/magiconair/properties v1.8.9 github.com/matthewhartstonge/argon2 v1.2.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
) )
require ( require (
github.com/cockroachdb/apd v1.1.0 // indirect github.com/cockroachdb/apd v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect
golang.org/x/crypto v0.33.0 // indirect github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

40
go.sum
View file

@ -4,12 +4,26 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
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/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
@ -18,10 +32,12 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/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/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= github.com/matthewhartstonge/argon2 v1.2.0 h1:oHo0H92JcmG4q5Ax6MuwDHa6iuJPz97RLwSfqcrjsSY=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/matthewhartstonge/argon2 v1.2.0/go.mod h1:2zMl2u3Ooe9zkpeU61cmcAJ4vgMC3YfvRbKWnPg0wAU=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -30,12 +46,24 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -0,0 +1,90 @@
package api
import (
"encoding/json"
"net/http"
"gitea.qpismont.fr/qpismont/trepa/internal/accounts/domain"
"gitea.qpismont.fr/qpismont/trepa/internal/core"
"github.com/go-playground/validator/v10"
)
var validate *validator.Validate
type Controller struct {
service domain.AccountService
}
func NewController(service domain.AccountService) Controller {
validate = validator.New()
return Controller{service: service}
}
func (c Controller) GetAccount(w *core.Response, r *http.Request) {
claims := r.Context().Value("claims").(core.JWTClaims)
account, httpErr := c.service.GetAccount(claims.AccountId)
if httpErr != nil {
w.WriteError(httpErr)
return
}
response := GetAccountResponse{
Id: account.Id,
Username: account.Username,
RoleId: account.RoleId,
CreatedAt: account.CreatedAt,
UpdatedAt: account.UpdatedAt,
}
responseJson, err := json.Marshal(response)
if err != nil {
w.WriteError(core.NewInternalServerError(err))
return
}
w.Json(responseJson)
}
func (c Controller) Login(w *core.Response, r *http.Request) {
var request LoginAccountRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
w.WriteError(core.ErrInvalidStruct)
return
}
if err := validate.Struct(request); err != nil {
w.WriteError(core.ErrInvalidStruct)
return
}
result, httpErr := c.service.Login(domain.AccountLogin{
Username: request.Username,
Password: request.Password,
})
if httpErr != nil {
w.WriteError(httpErr)
return
}
response := LoginAccountResponse{
Account: Account{
Id: result.Account.Id,
Username: result.Account.Username,
RoleId: result.Account.RoleId,
CreatedAt: result.Account.CreatedAt,
UpdatedAt: result.Account.UpdatedAt,
},
Token: result.Token,
}
responseJson, err := json.Marshal(response)
if err != nil {
w.WriteError(core.NewInternalServerError(err))
return
}
w.Json(responseJson)
}

View file

@ -0,0 +1,27 @@
package api
type LoginAccountRequest struct {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
}
type LoginAccountResponse struct {
Account Account `json:"account"`
Token string `json:"token"`
}
type Account struct {
Id int `json:"id"`
Username string `json:"username"`
RoleId int `json:"role_id"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type GetAccountResponse struct {
Id int `json:"id"`
Username string `json:"username"`
RoleId int `json:"role_id"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}

View file

@ -0,0 +1,17 @@
package api
import (
"gitea.qpismont.fr/qpismont/trepa/internal/accounts/repository"
"gitea.qpismont.fr/qpismont/trepa/internal/accounts/service"
"gitea.qpismont.fr/qpismont/trepa/internal/core"
"github.com/jmoiron/sqlx"
)
func BindRoutes(srv *core.ServerMux, db *sqlx.DB) {
repository := repository.NewRepository(db)
service := service.NewService(repository)
controller := NewController(service)
srv.HandleFunc("POST /accounts/login", controller.Login)
srv.HandleFunc("GET /accounts/me", core.JwtMiddleware(controller.GetAccount))
}

View file

@ -1,20 +0,0 @@
package accounts
import (
"net/http"
"gitea.qpismont.fr/qpismont/trepa/internal/core"
)
type Controller struct {
service Service
}
func NewController(service Service) Controller {
return Controller{service: service}
}
func (c *Controller) CreateAccount(w *core.Response, r *http.Request) {
w.WriteHeader(http.StatusCreated)
w.Json([]byte("test"))
}

View file

@ -1,4 +1,4 @@
package accounts package domain
type Account struct { type Account struct {
Id int `db:"id" json:"id"` Id int `db:"id" json:"id"`
@ -8,3 +8,25 @@ type Account struct {
CreatedAt string `db:"created_at" json:"created_at"` CreatedAt string `db:"created_at" json:"created_at"`
UpdatedAt string `db:"updated_at" json:"updated_at"` UpdatedAt string `db:"updated_at" json:"updated_at"`
} }
type AccountLogin struct {
Username string
Password string
}
type AccountWithToken struct {
Account *Account
Token string
}
type AccountCreate struct {
Username string
Password string
RoleId int
}
type AccountRegister struct {
Username string
Password string
RoleId int
}

View file

@ -1,4 +1,4 @@
package accounts package domain
import ( import (
"net/http" "net/http"

View file

@ -0,0 +1,7 @@
package domain
type AccountRepository interface {
Insert(account Account) (int, error)
FetchOneByUsername(username string) (*Account, error)
FetchOneById(id int) (*Account, error)
}

View file

@ -0,0 +1,9 @@
package domain
import "gitea.qpismont.fr/qpismont/trepa/internal/core"
type AccountService interface {
Login(login AccountLogin) (*AccountWithToken, *core.HTTPError)
Register(register AccountRegister) (int, *core.HTTPError)
GetAccount(id int) (*Account, *core.HTTPError)
}

View file

@ -1,48 +0,0 @@
package accounts
import (
"database/sql"
"github.com/jmoiron/sqlx"
)
type Repository struct {
db *sqlx.DB
}
func NewRepository(db *sqlx.DB) Repository {
return Repository{db: db}
}
func (r *Repository) Insert(account *Account) (int, error) {
var id int
stmt, err := r.db.Prepare(SqlInsert)
if err != nil {
return id, err
}
defer stmt.Close()
err = stmt.QueryRow(account.Username, account.Password, account.RoleId).Scan(&id)
if err != nil {
return id, err
}
return id, nil
}
func (r *Repository) FetchOneByUsername(username string) (*Account, error) {
var account Account
err := r.db.Get(&account, SqlFetchOneByUsername, username)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
} else {
return nil, err
}
}
return &account, nil
}

View file

@ -0,0 +1,64 @@
package repository
import (
"database/sql"
"gitea.qpismont.fr/qpismont/trepa/internal/accounts/domain"
"github.com/jmoiron/sqlx"
)
type Repository struct {
db *sqlx.DB
}
func NewRepository(db *sqlx.DB) domain.AccountRepository {
return &Repository{db: db}
}
func (r *Repository) Insert(account domain.Account) (int, error) {
var id int
stmt, err := r.db.Prepare(SqlInsert)
if err != nil {
return id, err
}
defer stmt.Close()
err = stmt.QueryRow(account.Username, account.Password, account.RoleId).Scan(&id)
if err != nil {
return id, err
}
return id, nil
}
func (r *Repository) FetchOneById(id int) (*domain.Account, error) {
var account domain.Account
err := r.db.Get(&account, SqlFetchOneById, id)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
} else {
return nil, err
}
}
return &account, nil
}
func (r *Repository) FetchOneByUsername(username string) (*domain.Account, error) {
var account domain.Account
err := r.db.Get(&account, SqlFetchOneByUsername, username)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
} else {
return nil, err
}
}
return &account, nil
}

View file

@ -0,0 +1,69 @@
package repository
import (
"testing"
"gitea.qpismont.fr/qpismont/trepa/internal/accounts/domain"
"gitea.qpismont.fr/qpismont/trepa/test"
"github.com/stretchr/testify/assert"
)
func TestRepository_Insert(t *testing.T) {
db := test.SetupTestDB(t, "../../..")
defer db.Close()
repo := NewRepository(db)
account := domain.Account{
Username: "test",
Password: "test",
RoleId: 1,
}
id, err := repo.Insert(account)
if err != nil {
t.Fatalf("Failed to insert account: %v", err)
}
assert.Equal(t, id, 3)
}
func TestRepository_FetchOneByUsername(t *testing.T) {
db := test.SetupTestDB(t, "../../..")
defer db.Close()
repo := NewRepository(db)
account, err := repo.FetchOneByUsername("admin")
if err != nil {
t.Fatalf("Failed to fetch account: %v", err)
}
if account == nil {
t.Fatalf("Account not found")
}
assert.Equal(t, "admin", account.Username)
assert.NotEmpty(t, account.Password)
assert.Equal(t, 1, account.RoleId)
}
func TestRepository_FetchOneById(t *testing.T) {
db := test.SetupTestDB(t, "../../..")
defer db.Close()
repo := NewRepository(db)
account, err := repo.FetchOneById(1)
if err != nil {
t.Fatalf("Failed to fetch account: %v", err)
}
if account == nil {
t.Fatalf("Account not found")
}
assert.Equal(t, "admin", account.Username)
assert.NotEmpty(t, account.Password)
assert.Equal(t, 1, account.RoleId)
}

View file

@ -1,6 +1,7 @@
package accounts package repository
const ( const (
SqlInsert = "INSERT INTO accounts (username, password, role_id) VALUES ($1, $2, $3) RETURNING id" SqlInsert = "INSERT INTO accounts (username, password, role_id) VALUES ($1, $2, $3) RETURNING id"
SqlFetchOneByUsername = "SELECT * FROM accounts WHERE username = $1" SqlFetchOneByUsername = "SELECT * FROM accounts WHERE username = $1"
SqlFetchOneById = "SELECT * FROM accounts WHERE id = $1"
) )

View file

@ -1,48 +0,0 @@
package accounts
import (
"testing"
"gitea.qpismont.fr/qpismont/trepa/test"
"github.com/magiconair/properties/assert"
)
func TestRepository_Insert(t *testing.T) {
db := test.SetupTestDB(t, "../..")
defer db.Close()
repo := NewRepository(db)
account := &Account{
Username: "test",
Password: "test",
RoleId: 1,
}
id, err := repo.Insert(account)
if err != nil {
t.Fatalf("Failed to insert account: %v", err)
}
assert.Equal(t, id, 3)
}
func TestRepository_FetchOneByUsername(t *testing.T) {
db := test.SetupTestDB(t, "../..")
defer db.Close()
repo := NewRepository(db)
account, err := repo.FetchOneByUsername("admin")
if err != nil {
t.Fatalf("Failed to fetch account: %v", err)
}
if account == nil {
t.Fatalf("Account not found")
}
assert.Equal(t, account.Username, "admin")
assert.Equal(t, account.Password, "LOLPASSWORD")
assert.Equal(t, account.RoleId, 1)
}

View file

@ -1,14 +0,0 @@
package accounts
import (
"gitea.qpismont.fr/qpismont/trepa/internal/core"
"github.com/jmoiron/sqlx"
)
func BindRoutes(srv *core.ServerMux, db *sqlx.DB) {
repository := NewRepository(db)
service := NewService(repository)
controller := NewController(service)
srv.HandleFunc("GET /accounts", controller.CreateAccount)
}

View file

@ -1,9 +0,0 @@
package accounts
type Service struct {
repository Repository
}
func NewService(repository Repository) Service {
return Service{repository: repository}
}

View file

@ -0,0 +1,91 @@
package service
import (
"gitea.qpismont.fr/qpismont/trepa/internal/accounts/domain"
"gitea.qpismont.fr/qpismont/trepa/internal/core"
)
type Service struct {
repository domain.AccountRepository
}
func NewService(repository domain.AccountRepository) domain.AccountService {
return &Service{repository: repository}
}
func (s *Service) GetAccount(id int) (*domain.Account, *core.HTTPError) {
account, err := s.repository.FetchOneById(id)
if err != nil {
return nil, core.NewInternalServerError(err)
}
if account == nil {
return nil, domain.ErrAccountNotFound
}
return account, nil
}
func (s *Service) Login(login domain.AccountLogin) (*domain.AccountWithToken, *core.HTTPError) {
account, err := s.repository.FetchOneByUsername(login.Username)
if err != nil {
return nil, core.NewInternalServerError(err)
}
if account == nil {
return nil, domain.ErrAccountNotFound
}
ok, err := core.ComparePassword(login.Password, account.Password)
if err != nil {
return nil, core.NewInternalServerError(err)
}
if !ok {
return nil, domain.ErrBadPassword
}
claims := core.JWTClaims{
AccountId: account.Id,
RoleId: account.RoleId,
}
token, err := core.SignJWT(claims)
if err != nil {
return nil, core.NewInternalServerError(err)
}
return &domain.AccountWithToken{
Account: account,
Token: token,
}, nil
}
func (s *Service) Register(register domain.AccountRegister) (int, *core.HTTPError) {
accountExist, err := s.repository.FetchOneByUsername(register.Username)
if err != nil {
return 0, core.NewInternalServerError(err)
}
if accountExist != nil {
return 0, domain.ErrAccountAlreadyExists
}
hashedPassword, err := core.HashPassword(register.Password)
if err != nil {
return 0, core.NewInternalServerError(err)
}
account := domain.Account{
Username: register.Username,
Password: hashedPassword,
RoleId: register.RoleId,
}
id, err := s.repository.Insert(account)
if err != nil {
return 0, core.NewInternalServerError(err)
}
return id, nil
}

View file

@ -0,0 +1,122 @@
package service
import (
"testing"
"gitea.qpismont.fr/qpismont/trepa/internal/accounts/domain"
"gitea.qpismont.fr/qpismont/trepa/internal/accounts/repository"
"gitea.qpismont.fr/qpismont/trepa/internal/core"
"gitea.qpismont.fr/qpismont/trepa/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockRepository struct {
mock.Mock
}
func (m *MockRepository) FetchOneById(id int) (*domain.Account, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*domain.Account), args.Error(1)
}
func (m *MockRepository) FetchOneByUsername(username string) (*domain.Account, error) {
args := m.Called(username)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*domain.Account), args.Error(1)
}
func (m *MockRepository) Insert(account domain.Account) (int, error) {
args := m.Called(account)
return args.Int(0), args.Error(1)
}
func (m *MockRepository) FetchOneByRoleId(roleId int) (*domain.Account, error) {
args := m.Called(roleId)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*domain.Account), args.Error(1)
}
func TestService_Register(t *testing.T) {
mockRepo := new(MockRepository)
mockRepo.On("FetchOneByUsername", "admin").Return(nil, nil)
mockRepo.On("Insert", mock.Anything).Return(1, nil)
service := NewService(mockRepo)
id, _ := service.Register(domain.AccountRegister{
Username: "admin",
Password: "admin",
RoleId: 1,
})
assert.Equal(t, 1, id)
mockRepo.AssertExpectations(t)
}
func TestService_GetAccount(t *testing.T) {
db := test.SetupTestDB(t, "../../..")
defer db.Close()
repo := repository.NewRepository(db)
service := NewService(repo)
account, err := service.GetAccount(1)
if err != nil {
t.Fatalf("Failed to get account: %v", err)
}
if account == nil {
t.Fatalf("Account not found")
}
assert.Equal(t, "admin", account.Username)
assert.Equal(t, 1, account.RoleId)
assert.NotEmpty(t, account.Password)
}
func TestService_Login(t *testing.T) {
mockRepo := new(MockRepository)
testPassword := "testpassword"
hashedPassword, _ := core.HashPassword(testPassword)
mockRepo.On("FetchOneByUsername", "admin").Return(&domain.Account{
Id: 1,
Username: "admin",
Password: hashedPassword,
RoleId: 1,
}, nil)
service := NewService(mockRepo)
result, err := service.Login(domain.AccountLogin{
Username: "admin",
Password: testPassword,
})
if err != nil {
t.Fatalf("Failed to login: %v", err)
}
assert.NotNil(t, result)
assert.NotNil(t, result.Token)
assert.NotNil(t, result.Account)
assert.Equal(t, "admin", result.Account.Username)
assert.Equal(t, 1, result.Account.RoleId)
mockRepo.AssertExpectations(t)
}

View file

@ -1,11 +1,13 @@
package core package core
import ( import (
"encoding/json"
"net/http" "net/http"
) )
var ( var (
ErrInvalidToken = NewHTTPError(http.StatusUnauthorized, "Invalid token", nil) ErrInvalidToken = NewHTTPError(http.StatusUnauthorized, "Invalid token", nil)
ErrInvalidStruct = NewHTTPError(http.StatusBadRequest, "Invalid struct", nil)
) )
type HTTPError struct { type HTTPError struct {
@ -23,6 +25,14 @@ func (e *HTTPError) Unwrap() error {
return e.Cause return e.Cause
} }
func (e *HTTPError) IntoJsonBytes() []byte {
json, err := json.Marshal(e)
if err != nil {
return nil
}
return json
}
func NewHTTPError(code int, message string, cause error) *HTTPError { func NewHTTPError(code int, message string, cause error) *HTTPError {
details := "" details := ""
if cause != nil { if cause != nil {

29
internal/core/hash.go Normal file
View file

@ -0,0 +1,29 @@
package core
import (
"github.com/matthewhartstonge/argon2"
)
func HashPassword(password string) (string, error) {
argon := instanceArgon2()
hash, err := argon.HashEncoded([]byte(password))
if err != nil {
return "", err
}
return string(hash), nil
}
func ComparePassword(password string, hash string) (bool, error) {
ok, err := argon2.VerifyEncoded([]byte(password), []byte(hash))
if err != nil {
return false, err
}
return ok, nil
}
func instanceArgon2() argon2.Config {
return argon2.DefaultConfig()
}

View file

@ -0,0 +1,18 @@
package core
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestHashPassword(t *testing.T) {
password := "password"
hashedPassword, err := HashPassword(password)
assert.NoError(t, err)
hashedOk, err := ComparePassword(password, hashedPassword)
assert.NoError(t, err)
assert.True(t, hashedOk)
}

View file

@ -26,6 +26,12 @@ func (r *Response) WriteHeader(status int) {
r.Status = status r.Status = status
} }
func (r *Response) WriteError(err *HTTPError) {
r.Header().Set("Content-Type", "application/json")
r.WriteHeader(err.Code)
r.Write(err.IntoJsonBytes())
}
func (r *Response) Json(json []byte) { func (r *Response) Json(json []byte) {
r.Header().Set("Content-Type", "application/json") r.Header().Set("Content-Type", "application/json")
r.Write(json) r.Write(json)

View file

@ -22,7 +22,7 @@ func SignJWT(claims JWTClaims) (string, error) {
return token.SignedString([]byte(jwtSecret)) return token.SignedString([]byte(jwtSecret))
} }
func VerifyJWT(token string) (JWTClaims, error) { func VerifyJWT(token string) (JWTClaims, *HTTPError) {
parsedClaims := JWTClaims{} parsedClaims := JWTClaims{}
claims, err := jwt.ParseWithClaims(token, &parsedClaims, func(token *jwt.Token) (any, error) { claims, err := jwt.ParseWithClaims(token, &parsedClaims, func(token *jwt.Token) (any, error) {
return []byte(jwtSecret), nil return []byte(jwtSecret), nil

View file

@ -19,9 +19,9 @@ func TestJWT_GenerateToken(t *testing.T) {
assert.NotEmpty(t, token) assert.NotEmpty(t, token)
claims, err := VerifyJWT(token) claims, jwtErr := VerifyJWT(token)
if err != nil { if jwtErr != nil {
t.Fatalf("Failed to verify token: %v", err) t.Fatalf("Failed to verify token: %v", jwtErr)
} }
assert.Equal(t, claims.AccountId, 1) assert.Equal(t, claims.AccountId, 1)

View file

@ -44,3 +44,21 @@ func ErrorMiddleware(next func(w *Response, r *http.Request)) func(w *Response,
next(w, r) next(w, r)
} }
} }
func JwtMiddleware(next func(w *Response, r *http.Request)) func(w *Response, r *http.Request) {
return func(w *Response, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
w.WriteError(ErrInvalidToken)
return
}
_, err := VerifyJWT(token)
if err != nil {
w.WriteError(err)
return
}
next(w, r)
}
}

View file

@ -0,0 +1,40 @@
package api
import (
"encoding/json"
"net/http"
"gitea.qpismont.fr/qpismont/trepa/internal/core"
"gitea.qpismont.fr/qpismont/trepa/internal/variables/domain"
)
type Controller struct {
service domain.VariableService
}
func NewController(service domain.VariableService) Controller {
return Controller{service: service}
}
func (c Controller) GetVariableByName(w *core.Response, r *http.Request) {
name := r.PathValue("name")
variable, httpErr := c.service.GetVariable(name)
if httpErr != nil {
w.WriteError(httpErr)
return
}
response := GetVariableResponse{
Name: variable.Name,
Value: variable.Value,
}
responseJson, err := json.Marshal(response)
if err != nil {
w.WriteError(core.NewInternalServerError(err))
return
}
w.Json(responseJson)
}

View file

@ -0,0 +1,6 @@
package api
type GetVariableResponse struct {
Name string `json:"name"`
Value string `json:"value"`
}

View file

@ -0,0 +1,16 @@
package api
import (
"gitea.qpismont.fr/qpismont/trepa/internal/core"
"gitea.qpismont.fr/qpismont/trepa/internal/variables/repository"
"gitea.qpismont.fr/qpismont/trepa/internal/variables/service"
"github.com/jmoiron/sqlx"
)
func BindRoutes(srv *core.ServerMux, db *sqlx.DB) {
repository := repository.NewRepository(db)
service := service.NewService(repository)
controller := NewController(service)
srv.HandleFunc("GET /variables/{name}", controller.GetVariableByName)
}

View file

@ -0,0 +1,9 @@
package domain
import (
"net/http"
"gitea.qpismont.fr/qpismont/trepa/internal/core"
)
var ErrVariableNotFound = core.NewHTTPError(http.StatusNotFound, "variable not found", nil)

View file

@ -0,0 +1,5 @@
package domain
type VariableRepository interface {
FetchOneByName(name string) (*Variable, error)
}

View file

@ -0,0 +1,7 @@
package domain
import "gitea.qpismont.fr/qpismont/trepa/internal/core"
type VariableService interface {
GetVariable(name string) (*Variable, *core.HTTPError)
}

View file

@ -0,0 +1,11 @@
package domain
import "time"
type Variable struct {
Id int `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Value string `db:"value" json:"value"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

View file

@ -0,0 +1,5 @@
package repository
const (
SqlFetchOneByName = "SELECT * FROM variables WHERE name = $1"
)

View file

@ -0,0 +1,24 @@
package repository
import (
"gitea.qpismont.fr/qpismont/trepa/internal/variables/domain"
"github.com/jmoiron/sqlx"
)
type VariableRepository struct {
db *sqlx.DB
}
func NewRepository(db *sqlx.DB) *VariableRepository {
return &VariableRepository{db: db}
}
func (r *VariableRepository) FetchOneByName(name string) (*domain.Variable, error) {
var variable domain.Variable
err := r.db.Get(&variable, "SELECT * FROM variables WHERE name = $1", name)
if err != nil {
return nil, err
}
return &variable, nil
}

View file

@ -0,0 +1,20 @@
package repository
import (
"testing"
"gitea.qpismont.fr/qpismont/trepa/test"
"github.com/stretchr/testify/assert"
)
func TestVariableRepository_FetchOneByName(t *testing.T) {
db := test.SetupTestDB(t, "../../..")
defer db.Close()
repository := NewRepository(db)
variable, err := repository.FetchOneByName("first_account_created")
assert.NoError(t, err)
assert.Equal(t, "first_account_created", variable.Name)
assert.Equal(t, "false", variable.Value)
}

View file

@ -0,0 +1,27 @@
package service
import (
"gitea.qpismont.fr/qpismont/trepa/internal/core"
"gitea.qpismont.fr/qpismont/trepa/internal/variables/domain"
)
type VariableService struct {
repository domain.VariableRepository
}
func NewService(repository domain.VariableRepository) *VariableService {
return &VariableService{repository: repository}
}
func (s *VariableService) GetVariable(name string) (*domain.Variable, *core.HTTPError) {
variable, err := s.repository.FetchOneByName(name)
if err != nil {
return nil, core.NewInternalServerError(err)
}
if variable == nil {
return nil, domain.ErrVariableNotFound
}
return variable, nil
}

View file

@ -0,0 +1,24 @@
package service
import (
"testing"
"gitea.qpismont.fr/qpismont/trepa/internal/variables/repository"
"gitea.qpismont.fr/qpismont/trepa/test"
"github.com/stretchr/testify/assert"
)
func TestVariableService_GetVariable(t *testing.T) {
db := test.SetupTestDB(t, "../../..")
defer db.Close()
repository := repository.NewRepository(db)
service := NewService(repository)
variable, _ := service.GetVariable("first_account_created")
assert.NotNil(t, variable)
assert.Equal(t, 1, variable.Id)
assert.Equal(t, "first_account_created", variable.Name)
assert.Equal(t, "false", variable.Value)
}

View file

@ -0,0 +1,14 @@
CREATE TABLE public.variables
(
id integer NOT NULL GENERATED ALWAYS AS IDENTITY,
name text,
value text,
created_at timestamp without time zone NOT NULL DEFAULT now(),
updated_at timestamp without time zone NOT NULL DEFAULT now(),
PRIMARY KEY (id)
);
ALTER TABLE IF EXISTS public.variables
OWNER to dev;
INSERT INTO public.variables (name, value) VALUES ('first_account_created', 'false');

View file

@ -0,0 +1,5 @@
#!/bin/bash
timestamp=$(date +%Y%m%d%H%M%S)
migration_file="migrations/$(date +%Y%m%d%H%M%S)_$1.up.sql"
touch $migration_file

9
scripts/migrate.sh Normal file
View file

@ -0,0 +1,9 @@
#!/bin/bash
if ! command -v migrate &> /dev/null; then
echo "migrate could not be found"
exit 1
fi
migrate -path ./migrations -database "$DATABASE_URL" -verbose up

View file

@ -1,2 +1,2 @@
INSERT INTO accounts (username, password, role_id) VALUES ('admin', 'LOLPASSWORD', 1); INSERT INTO accounts (username, password, role_id) VALUES ('admin', '##TEST_PASSWORD_HASH##', 1);
INSERT INTO accounts (username, password, role_id) VALUES ('user', 'LOLPASSWORD', 2); INSERT INTO accounts (username, password, role_id) VALUES ('user', '##TEST_PASSWORD_HASH##', 2);

1
test/fixtures/01-variables.sql vendored Normal file
View file

@ -0,0 +1 @@
INSERT INTO variables (name, value) VALUES ('first_account_created', 'false');