Compare commits
17 Commits
d3a8d74714
...
a7d2b5c9f2
Author | SHA1 | Date |
---|---|---|
Renovate | a7d2b5c9f2 | |
Dawid Wysokiński | 5777aee059 | |
Dawid Wysokiński | c71f24c00d | |
Dawid Wysokiński | 900e7abc58 | |
Dawid Wysokiński | 0d6ee87cae | |
Dawid Wysokiński | a67b911e77 | |
Dawid Wysokiński | b8bd8cd4df | |
renovate | d81f7e4945 | |
Dawid Wysokiński | 3adb5f910a | |
Dawid Wysokiński | d7176066f4 | |
Dawid Wysokiński | 847ac51b38 | |
Dawid Wysokiński | 5f144a9fd2 | |
Dawid Wysokiński | 026db8e50f | |
Dawid Wysokiński | 037e43b299 | |
Dawid Wysokiński | d24d43bfd2 | |
Dawid Wysokiński | 10cb5b83b1 | |
Dawid Wysokiński | 786cc60e38 |
116
.drone.yml
116
.drone.yml
|
@ -63,4 +63,118 @@ trigger:
|
|||
- push
|
||||
- pull_request
|
||||
branch:
|
||||
- master
|
||||
- master
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: govulncheck
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: govulncheck
|
||||
image: golang:1.19
|
||||
commands:
|
||||
- make generate
|
||||
- go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
- govulncheck ./...
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- cron
|
||||
cron:
|
||||
- govulncheck
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: linux-amd64
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: publish
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
registry: gitea.dwysokinski.me
|
||||
repo: gitea.dwysokinski.me/twhelp-packages/sessions
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-amd64
|
||||
dockerfile: ./build/docker/sessions/prod/Dockerfile
|
||||
build_args_from_env: [DRONE_TAG]
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: manifest
|
||||
|
||||
steps:
|
||||
- name: manifest
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
auto_tag: "true"
|
||||
ignore_missing: "true"
|
||||
spec: ./build/docker/sessions/prod/manifest.tmpl
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
- name: manifest-latest
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
tags: latest
|
||||
ignore_missing: "true"
|
||||
spec: ./build/docker/sessions/prod/manifest.tmpl
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
|
||||
depends_on:
|
||||
- linux-amd64
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: deploy
|
||||
|
||||
steps:
|
||||
- name: deploy-k8s
|
||||
image: alpine/k8s:1.25.3
|
||||
environment:
|
||||
KUBECONFIG:
|
||||
from_secret: kubeconfig
|
||||
commands:
|
||||
- "mkdir ~/.kube && echo \"$KUBECONFIG\" > ~/.kube/twhelp"
|
||||
- "cd ./k8s/overlays/prod && kustomize edit set image sessions=gitea.dwysokinski.me/twhelp-packages/sessions:${DRONE_TAG##v} && cd ../../.."
|
||||
- "kubectl --kubeconfig ~/.kube/twhelp -n twhelp delete jobs.batch sessions-migrations-job || true"
|
||||
- kustomize build ./k8s/overlays/prod | kubectl --kubeconfig ~/.kube/twhelp apply -n twhelp -f -
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
|
||||
depends_on:
|
||||
- manifest
|
||||
---
|
||||
kind: signature
|
||||
hmac: 7262c4a4ae8fd7dfd0f31bd3d1177a42d0b116fc4c9737573c1207072135c14b
|
||||
|
||||
...
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022 Dawid Wysokiński
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,87 @@
|
|||
# sessions
|
||||
|
||||
A minimalist backend for [sessions-ext](https://gitea.dwysokinski.me/twhelp/sessions-ext).
|
||||
|
||||
## Getting started
|
||||
|
||||
### Development
|
||||
|
||||
#### Setting up the environment
|
||||
|
||||
Prerequisites:
|
||||
|
||||
1. **Go** (>= 1.19)
|
||||
2. **Node.js** (LTS, needed for commitlint)
|
||||
3. **Kubernetes** (>= 1.25.0)
|
||||
1. **minikube**
|
||||
2. **Docker Desktop**
|
||||
4. **Docker CLI**
|
||||
5. **Skaffold**
|
||||
6. **pre-commit**
|
||||
7. **IDE/Code editor** (e.g. Goland, VSCode, vim, neovim)
|
||||
8. **direnv** (optional, but recommended)
|
||||
|
||||
```shell
|
||||
# if you have direnv installed
|
||||
direnv allow
|
||||
|
||||
# install git hooks and required tools
|
||||
make install
|
||||
|
||||
# run all required services
|
||||
skaffold run --port-forward=true --tail=true
|
||||
|
||||
# stop all of them
|
||||
skaffold delete
|
||||
```
|
||||
|
||||
#### Running unit tests
|
||||
|
||||
At least one of the following is required to run unit tests:
|
||||
- Docker ([dockertest](https://github.com/ory/dockertest) will spin up a database)
|
||||
- Postgres database
|
||||
|
||||
```shell
|
||||
# Docker
|
||||
go test -v ./...
|
||||
|
||||
# Postgres database
|
||||
TESTS_DB_DSN=postgres://sessions:sessions_pass@127.0.0.1/sessions go test -v ./...
|
||||
```
|
||||
|
||||
#### Creating a new database migration
|
||||
|
||||
```shell
|
||||
# create a migration
|
||||
go run ./cmd/sessions/main.go db migration create go migration 1
|
||||
```
|
||||
|
||||
![img.png](docs/migration.png)
|
||||
|
||||
## Configuration options
|
||||
|
||||
Configuration options can be specified via environment variables.
|
||||
|
||||
| Env variable | Default | Description |
|
||||
|--------------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------|
|
||||
| ``APP_MODE`` | ``development`` | Whether to use development or production settings |
|
||||
| ``DB_DSN`` | | **Required.** Syntax: ``postgres://user:password@host:5432/db?sslmode=disable``. Only Postgres is supported. |
|
||||
| ``DB_MAX_OPEN_CONNECTIONS`` | ``5`` | Maximum number of open connections to the database (https://pkg.go.dev/database/sql#DB.SetMaxOpenConns) |
|
||||
| ``DB_MAX_IDLE_CONNECTIONS`` | ``2`` | Maximum number of connections in the idle connection pool (https://pkg.go.dev/database/sql#DB.SetMaxIdleConns) |
|
||||
| ``DB_CONNECTION_MAX_LIFETIME`` | ``3m`` | Maximum amount of time a connection may be reused (https://pkg.go.dev/database/sql#DB.SetConnMaxLifetime) |
|
||||
| ``API_SWAGGER_ENABLED`` | ``false`` | Enables the API documentation endpoints (e.g. ``/api/v1/swagger/index.html``, ``/api/v1/swagger/doc.json``) |
|
||||
| ``API_SWAGGER_HOST`` | | Host (name or ip) serving the API (e.g. localhost:8080) |
|
||||
| ``API_SWAGGER_SCHEMES`` | ``http,https`` | Comma-separated list of protocols |
|
||||
| ``API_CORS_ENABLED`` | ``false`` | Enables CORS headers |
|
||||
| ``API_CORS_ALLOWED_ORIGINS`` | | Comma-separated list of allowed domains |
|
||||
| ``API_CORS_ALLOW_CREDENTIALS`` | ``false`` | Whether requests with credentials are allowed |
|
||||
| ``API_CORS_ALLOWED_METHODS`` | ``HEAD,GET,POST,PUT`` | Comma-separated list of allowed methods |
|
||||
| ``API_CORS_MAX_AGE`` | ``300`` | Max time to cache response (seconds) |
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the MIT License. See ``LICENSE`` for more information.
|
||||
|
||||
## Contact
|
||||
|
||||
Dawid Wysokiński - [contact@dwysokinski.me](mailto:contact@dwysokinski.me)
|
|
@ -0,0 +1,24 @@
|
|||
FROM golang:1.19.4-alpine3.16 as builder
|
||||
|
||||
WORKDIR /sessions
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
RUN apk --no-cache add make
|
||||
|
||||
COPY . .
|
||||
RUN make generate
|
||||
ARG DRONE_TAG="development"
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-X main.version=${DRONE_TAG##v}" -trimpath -o sessions ./cmd/sessions/main.go
|
||||
|
||||
######## Start a new stage from scratch #######
|
||||
FROM alpine:3.16
|
||||
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
COPY --from=builder /sessions/sessions /usr/bin/
|
||||
|
||||
EXPOSE 9234/tcp
|
||||
|
||||
ENTRYPOINT ["sessions"]
|
|
@ -0,0 +1,13 @@
|
|||
image: gitea.dwysokinski.me/twhelp-packages/sessions:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
|
||||
{{#if build.tags}}
|
||||
tags:
|
||||
{{#each build.tags}}
|
||||
- {{this}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
manifests:
|
||||
-
|
||||
image: gitea.dwysokinski.me/twhelp-packages/sessions:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
|
@ -44,7 +44,8 @@ func newCreateAPIKeyCmd() *cli.Command {
|
|||
ctx, cancel := context.WithTimeout(c.Context, createAPIKeyTimeout)
|
||||
defer cancel()
|
||||
|
||||
ak, err := service.NewAPIKey(bundb.NewAPIKey(db), service.NewUser(bundb.NewUser(db))).Create(ctx, c.Int64("userId"))
|
||||
svc := service.NewAPIKey(bundb.NewAPIKey(db), service.NewUser(bundb.NewUser(db)))
|
||||
ak, err := svc.Create(ctx, c.String("name"), c.Int64("userId"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("APIKeyService.Create: %w", err)
|
||||
}
|
||||
|
@ -54,6 +55,10 @@ func newCreateAPIKeyCmd() *cli.Command {
|
|||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Required: true,
|
||||
},
|
||||
&cli.Int64Flag{
|
||||
Name: "userId",
|
||||
Required: true,
|
||||
|
|
|
@ -40,6 +40,11 @@ func newMigrateCmd() *cli.Command {
|
|||
}()
|
||||
|
||||
migrator := migrations.NewMigrator(db)
|
||||
|
||||
if err = migrator.Init(c.Context); err != nil {
|
||||
return fmt.Errorf("migrator.Init: %w", err)
|
||||
}
|
||||
|
||||
if err = migrator.Lock(c.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -47,10 +52,6 @@ func newMigrateCmd() *cli.Command {
|
|||
_ = migrator.Unlock(c.Context)
|
||||
}()
|
||||
|
||||
if err = migrator.Init(c.Context); err != nil {
|
||||
return fmt.Errorf("migrator.Init: %w", err)
|
||||
}
|
||||
|
||||
group, err := migrator.Migrate(c.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrator.Migrate: %w", err)
|
||||
|
|
|
@ -130,13 +130,13 @@ func newServer(cfg serverConfig) (*http.Server, error) {
|
|||
}
|
||||
|
||||
type apiConfig struct {
|
||||
SwaggerEnabled bool `envconfig:"SWAGGER_ENABLED" default:"true"`
|
||||
SwaggerEnabled bool `envconfig:"SWAGGER_ENABLED" default:"false"`
|
||||
SwaggerHost string `envconfig:"SWAGGER_HOST" default:""`
|
||||
SwaggerSchemes []string `envconfig:"SWAGGER_SCHEMES" default:"http,https"`
|
||||
CORSEnabled bool `envconfig:"CORS_ENABLED" default:"false"`
|
||||
CORSAllowedOrigins []string `envconfig:"CORS_ALLOWED_ORIGINS" default:""`
|
||||
CORSAllowCredentials bool `envconfig:"CORS_ALLOW_CREDENTIALS" default:"false"`
|
||||
CORSAllowedMethods []string `envconfig:"CORS_ALLOWED_METHODS" default:"HEAD,GET"`
|
||||
CORSAllowedMethods []string `envconfig:"CORS_ALLOWED_METHODS" default:"HEAD,GET,POST,PUT"`
|
||||
CORSMaxAge uint32 `envconfig:"CORS_MAX_AGE" default:"300"`
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
8
go.mod
8
go.mod
|
@ -5,7 +5,7 @@ go 1.19
|
|||
require (
|
||||
gitea.dwysokinski.me/Kichiyaki/chizap v0.2.1
|
||||
github.com/cenkalti/backoff/v4 v4.2.0
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
github.com/go-chi/chi/v5 v5.0.8
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/uuid v1.3.0
|
||||
|
@ -14,13 +14,13 @@ require (
|
|||
github.com/ory/dockertest/v3 v3.9.1
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/swaggo/http-swagger v1.3.3
|
||||
github.com/swaggo/swag v1.8.8
|
||||
github.com/swaggo/swag v1.8.9
|
||||
github.com/uptrace/bun v1.1.9
|
||||
github.com/uptrace/bun/dbfixture v1.1.9
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.1.9
|
||||
github.com/uptrace/bun/driver/pgdriver v1.1.9
|
||||
github.com/urfave/cli/v2 v2.23.5
|
||||
go.uber.org/zap v1.23.0
|
||||
github.com/urfave/cli/v2 v2.23.7
|
||||
go.uber.org/zap v1.24.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
12
go.sum
12
go.sum
|
@ -37,8 +37,8 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh
|
|||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
|
||||
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
|
@ -151,8 +151,8 @@ github.com/uptrace/bun/dialect/pgdialect v1.1.9/go.mod h1:+ux7PjC4NYsNMdGE9b2ERx
|
|||
github.com/uptrace/bun/driver/pgdriver v1.1.9 h1:Dy9g/EpgOG15RP3mDAuJd/hnfXAUdBsfxpg9On27M+Y=
|
||||
github.com/uptrace/bun/driver/pgdriver v1.1.9/go.mod h1:YnPHzR4fT24PXrBTcadclXfdtkR9dYouqk2HwOiKf2g=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw=
|
||||
github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY=
|
||||
github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||
|
@ -175,8 +175,8 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
|||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
|
||||
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
|
||||
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
|
|
|
@ -24,12 +24,13 @@ func TestAPIKey_Create(t *testing.T) {
|
|||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params, err := domain.NewCreateAPIKeyParams(uuid.NewString(), fixture.user(t, "user-1").ID)
|
||||
params, err := domain.NewCreateAPIKeyParams(uuid.NewString(), uuid.NewString(), fixture.user(t, "user-1").ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
apiKey, err := repo.Create(context.Background(), params)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, apiKey.ID, int64(0))
|
||||
assert.Equal(t, params.Name(), apiKey.Name)
|
||||
assert.Equal(t, params.Key(), apiKey.Key)
|
||||
assert.Equal(t, params.UserID(), apiKey.UserID)
|
||||
assert.WithinDuration(t, time.Now(), apiKey.CreatedAt, time.Second)
|
||||
|
@ -38,7 +39,7 @@ func TestAPIKey_Create(t *testing.T) {
|
|||
t.Run("ERR: user doesn't exist", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params, err := domain.NewCreateAPIKeyParams(uuid.NewString(), fixture.user(t, "user-1").ID+1)
|
||||
params, err := domain.NewCreateAPIKeyParams(uuid.NewString(), uuid.NewString(), fixture.user(t, "user-1").ID+11111)
|
||||
require.NoError(t, err)
|
||||
|
||||
apiKey, err := repo.Create(context.Background(), params)
|
||||
|
@ -52,6 +53,7 @@ func TestAPIKey_Create(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
params, err := domain.NewCreateAPIKeyParams(
|
||||
uuid.NewString(),
|
||||
fixture.apiKey(t, "user-1-api-key-1").Key,
|
||||
fixture.user(t, "user-1").ID,
|
||||
)
|
||||
|
|
|
@ -35,6 +35,12 @@ func newDB(tb testing.TB) *bun.DB {
|
|||
return newDBWithDSN(tb, dsn)
|
||||
}
|
||||
|
||||
return newDBDockertest(tb)
|
||||
}
|
||||
|
||||
func newDBDockertest(tb testing.TB) *bun.DB {
|
||||
tb.Helper()
|
||||
|
||||
q := url.Values{}
|
||||
q.Add("sslmode", "disable")
|
||||
dsn := &url.URL{
|
||||
|
@ -162,6 +168,16 @@ func (f *bunfixture) apiKey(tb testing.TB, id string) domain.APIKey {
|
|||
return ak.ToDomain()
|
||||
}
|
||||
|
||||
func (f *bunfixture) session(tb testing.TB, id string) domain.Session {
|
||||
tb.Helper()
|
||||
|
||||
row, err := f.Row("Session." + id)
|
||||
require.NoError(tb, err)
|
||||
sess, ok := row.(*model.Session)
|
||||
require.True(tb, ok)
|
||||
return sess.ToDomain()
|
||||
}
|
||||
|
||||
func generateSchema() string {
|
||||
return strings.TrimFunc(strings.ReplaceAll(uuid.NewString(), "-", "_"), unicode.IsNumber)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ type APIKey struct {
|
|||
bun.BaseModel `bun:"base_model,table:api_keys,alias:ak"`
|
||||
|
||||
ID int64 `bun:"id,pk,autoincrement,identity"`
|
||||
Name string `bun:"name,nullzero,type:varchar(100),notnull"`
|
||||
Key string `bun:"key,nullzero,type:varchar(255),notnull,unique"`
|
||||
UserID int64 `bun:"user_id,nullzero,notnull"`
|
||||
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
|
||||
|
@ -18,6 +19,7 @@ type APIKey struct {
|
|||
|
||||
func NewAPIKey(p domain.CreateAPIKeyParams) APIKey {
|
||||
return APIKey{
|
||||
Name: p.Name(),
|
||||
Key: p.Key(),
|
||||
UserID: p.UserID(),
|
||||
CreatedAt: time.Now(),
|
||||
|
@ -27,6 +29,7 @@ func NewAPIKey(p domain.CreateAPIKeyParams) APIKey {
|
|||
func (a APIKey) ToDomain() domain.APIKey {
|
||||
return domain.APIKey{
|
||||
ID: a.ID,
|
||||
Name: a.Name,
|
||||
Key: a.Key,
|
||||
UserID: a.UserID,
|
||||
CreatedAt: a.CreatedAt,
|
||||
|
|
|
@ -14,7 +14,7 @@ func TestAPIKey(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
var id int64 = 123
|
||||
params, err := domain.NewCreateAPIKeyParams(uuid.NewString(), 1)
|
||||
params, err := domain.NewCreateAPIKeyParams(uuid.NewString(), uuid.NewString(), 1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
result := model.NewAPIKey(params)
|
||||
|
@ -22,6 +22,7 @@ func TestAPIKey(t *testing.T) {
|
|||
|
||||
apiKey := result.ToDomain()
|
||||
assert.Equal(t, id, apiKey.ID)
|
||||
assert.Equal(t, params.Name(), apiKey.Name)
|
||||
assert.Equal(t, params.Key(), apiKey.Key)
|
||||
assert.Equal(t, params.UserID(), apiKey.UserID)
|
||||
assert.WithinDuration(t, time.Now(), apiKey.CreatedAt, 10*time.Millisecond)
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/bundb/internal/model"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
|
||||
if _, err := tx.NewAddColumn().
|
||||
Model(&model.APIKey{}).
|
||||
ColumnExpr("name varchar(100)").
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return fmt.Errorf("api_keys - couldn't add the name column: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.NewUpdate().
|
||||
Model(&model.APIKey{}).
|
||||
Set("name = ?", "api key").
|
||||
Where("true").
|
||||
Exec(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("api_keys - name - couldn't set default value: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(ctx, "ALTER TABLE api_keys ALTER COLUMN name SET NOT NULL"); err != nil {
|
||||
return fmt.Errorf("api_keys - name - couldn't set not null: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}, func(ctx context.Context, db *bun.DB) error {
|
||||
if _, err := db.NewDropColumn().
|
||||
Model(&model.APIKey{}).
|
||||
Column("name").
|
||||
Exec(ctx); err != nil {
|
||||
return fmt.Errorf("api_keys - couldn't drop the name column: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
|
@ -2,6 +2,7 @@ package bundb
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
|
@ -20,10 +21,10 @@ func NewSession(db *bun.DB) *Session {
|
|||
return &Session{db: db}
|
||||
}
|
||||
|
||||
func (u *Session) CreateOrUpdate(ctx context.Context, params domain.CreateSessionParams) (domain.Session, error) {
|
||||
func (s *Session) CreateOrUpdate(ctx context.Context, params domain.CreateSessionParams) (domain.Session, error) {
|
||||
sess := model.NewSession(params)
|
||||
|
||||
if _, err := u.db.NewInsert().
|
||||
if _, err := s.db.NewInsert().
|
||||
Model(&sess).
|
||||
On("CONFLICT ON CONSTRAINT sessions_user_id_server_key_key DO UPDATE").
|
||||
Set("sid = EXCLUDED.sid").
|
||||
|
@ -39,6 +40,28 @@ func (u *Session) CreateOrUpdate(ctx context.Context, params domain.CreateSessio
|
|||
return sess.ToDomain(), nil
|
||||
}
|
||||
|
||||
func (s *Session) Get(ctx context.Context, userID int64, serverKey string) (domain.Session, error) {
|
||||
var sess model.Session
|
||||
|
||||
if err := s.db.NewSelect().
|
||||
Model(&sess).
|
||||
Where("user_id = ?", userID).
|
||||
Where("server_key = ?", serverKey).
|
||||
Scan(ctx); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return domain.Session{}, domain.SessionNotFoundError{ServerKey: serverKey}
|
||||
}
|
||||
return domain.Session{}, fmt.Errorf(
|
||||
"something went wrong while selecting sess (userID=%d,serverKey=%s) from the db: %w",
|
||||
userID,
|
||||
serverKey,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return sess.ToDomain(), nil
|
||||
}
|
||||
|
||||
func mapCreateSessionError(err error, params domain.CreateSessionParams) error {
|
||||
var pgError pgdriver.Error
|
||||
if !errors.As(err, &pgError) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package bundb_test
|
|||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -73,3 +74,52 @@ func TestSession_CreateOrUpdate(t *testing.T) {
|
|||
assert.Zero(t, sess)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession_Get(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := newDB(t)
|
||||
fixture := loadFixtures(t, db)
|
||||
repo := bundb.NewSession(db)
|
||||
sessFixture := fixture.session(t, "user-1-session-1")
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sess, err := repo.Get(context.Background(), sessFixture.UserID, sessFixture.ServerKey)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sessFixture, sess)
|
||||
})
|
||||
|
||||
t.Run("ERR: session not found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
userID int64
|
||||
serverKey string
|
||||
}{
|
||||
{
|
||||
userID: sessFixture.UserID + 11111,
|
||||
serverKey: sessFixture.ServerKey,
|
||||
},
|
||||
{
|
||||
userID: sessFixture.UserID,
|
||||
serverKey: sessFixture.ServerKey + "1111",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(fmt.Sprintf("UserID=%d,ServerKey=%s", tt.userID, tt.serverKey), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sess, err := repo.Get(context.Background(), tt.userID, tt.serverKey)
|
||||
assert.ErrorIs(t, err, domain.SessionNotFoundError{
|
||||
ServerKey: tt.serverKey,
|
||||
})
|
||||
assert.Zero(t, sess)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,10 +5,39 @@
|
|||
name: User-1
|
||||
name_lower: user-1
|
||||
created_at: 2022-03-15T15:00:10.000Z
|
||||
- _id: user-2
|
||||
id: 11112
|
||||
name: User-2
|
||||
name_lower: user-2
|
||||
created_at: 2022-04-15T15:00:10.000Z
|
||||
- model: APIKey
|
||||
rows:
|
||||
- _id: user-1-api-key-1
|
||||
id: 11111
|
||||
name: user-1-api-key-1
|
||||
key: 4c0d2d63-4ef7-4c23-bb4d-f3646cc9658f
|
||||
user_id: 11111
|
||||
created_at: 2022-03-15T15:15:10.000Z
|
||||
created_at: 2022-03-15T15:15:10.000Z
|
||||
- model: Session
|
||||
rows:
|
||||
- _id: user-1-session-1
|
||||
id: 111111
|
||||
user_id: 11111
|
||||
server_key: pl181
|
||||
sid: NzM3NjgzZmMtYWNlNC00MTI4LThiNWYtMWRlNTYwYjdjNmUx
|
||||
created_at: 2022-03-15T15:15:10.000Z
|
||||
updated_at: 2022-03-15T15:15:10.000Z
|
||||
- _id: user-1-session-2
|
||||
id: 111112
|
||||
user_id: 11111
|
||||
server_key: pl183
|
||||
sid: NzM3NjgzZmMtYWNlNC00MTI4LThiNWYtMWRlNTYwYjdjNmUx
|
||||
created_at: 2022-03-25T15:15:10.000Z
|
||||
updated_at: 2022-03-25T15:15:10.000Z
|
||||
- _id: user-2-session-1
|
||||
id: 111121
|
||||
user_id: 11112
|
||||
server_key: pl181
|
||||
sid: NzM3NjgzZmMtYWNlNC00MTI4LThiNWYtMWRlNTYwYjdjNmUx
|
||||
created_at: 2022-04-25T15:15:10.000Z
|
||||
updated_at: 2022-04-25T15:15:10.000Z
|
|
@ -80,9 +80,9 @@ func TestUser_Get(t *testing.T) {
|
|||
t.Run("ERR: user not found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
user, err := repo.Get(context.Background(), userFromFixture.ID+1)
|
||||
user, err := repo.Get(context.Background(), userFromFixture.ID+1111111)
|
||||
assert.ErrorIs(t, err, domain.UserNotFoundError{
|
||||
ID: userFromFixture.ID + 1,
|
||||
ID: userFromFixture.ID + 1111111,
|
||||
})
|
||||
assert.Zero(t, user)
|
||||
})
|
||||
|
|
|
@ -5,19 +5,41 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
apiKeyMaxLength = 100
|
||||
)
|
||||
|
||||
type APIKey struct {
|
||||
ID int64
|
||||
Name string
|
||||
Key string
|
||||
UserID int64
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type CreateAPIKeyParams struct {
|
||||
name string
|
||||
key string
|
||||
userID int64
|
||||
}
|
||||
|
||||
func NewCreateAPIKeyParams(key string, userID int64) (CreateAPIKeyParams, error) {
|
||||
func NewCreateAPIKeyParams(name, key string, userID int64) (CreateAPIKeyParams, error) {
|
||||
if name == "" {
|
||||
return CreateAPIKeyParams{}, ValidationError{
|
||||
Field: "Name",
|
||||
Err: ErrRequired,
|
||||
}
|
||||
}
|
||||
|
||||
if len(name) > apiKeyMaxLength {
|
||||
return CreateAPIKeyParams{}, ValidationError{
|
||||
Field: "Name",
|
||||
Err: MaxLengthError{
|
||||
Max: apiKeyMaxLength,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if key == "" {
|
||||
return CreateAPIKeyParams{}, ValidationError{
|
||||
Field: "Key",
|
||||
|
@ -34,7 +56,11 @@ func NewCreateAPIKeyParams(key string, userID int64) (CreateAPIKeyParams, error)
|
|||
}
|
||||
}
|
||||
|
||||
return CreateAPIKeyParams{key: key, userID: userID}, nil
|
||||
return CreateAPIKeyParams{name: name, key: key, userID: userID}, nil
|
||||
}
|
||||
|
||||
func (c CreateAPIKeyParams) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
func (c CreateAPIKeyParams) Key() string {
|
||||
|
|
|
@ -14,28 +14,52 @@ func TestNewCreateAPIKeyParams(t *testing.T) {
|
|||
|
||||
tests := []struct {
|
||||
name string
|
||||
keyName string
|
||||
key string
|
||||
userID int64
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "OK",
|
||||
keyName: uuid.NewString(),
|
||||
key: uuid.NewString(),
|
||||
userID: 1,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "ERR: username is required",
|
||||
key: "",
|
||||
name: "ERR: name is required",
|
||||
keyName: "",
|
||||
key: uuid.NewString(),
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "Name",
|
||||
Err: domain.ErrRequired,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: name is too long",
|
||||
keyName: randString(101),
|
||||
key: uuid.NewString(),
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "Name",
|
||||
Err: domain.MaxLengthError{
|
||||
Max: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: key is required",
|
||||
keyName: uuid.NewString(),
|
||||
key: "",
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "Key",
|
||||
Err: domain.ErrRequired,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: userID < 1",
|
||||
key: uuid.NewString(),
|
||||
userID: 0,
|
||||
name: "ERR: userID < 1",
|
||||
keyName: uuid.NewString(),
|
||||
key: uuid.NewString(),
|
||||
userID: 0,
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "UserID",
|
||||
Err: domain.MinError{
|
||||
|
@ -51,7 +75,7 @@ func TestNewCreateAPIKeyParams(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params, err := domain.NewCreateAPIKeyParams(tt.key, tt.userID)
|
||||
params, err := domain.NewCreateAPIKeyParams(tt.keyName, tt.key, tt.userID)
|
||||
if tt.expectedErr != nil {
|
||||
assert.Equal(t, tt.expectedErr, err)
|
||||
assert.Zero(t, params)
|
||||
|
|
|
@ -2,6 +2,7 @@ package domain
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -83,3 +84,19 @@ func isBase64(s string) bool {
|
|||
_, err := base64.StdEncoding.DecodeString(s)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
type SessionNotFoundError struct {
|
||||
ServerKey string
|
||||
}
|
||||
|
||||
func (e SessionNotFoundError) Error() string {
|
||||
return fmt.Sprintf("session (ServerKey=%s) not found", e.ServerKey)
|
||||
}
|
||||
|
||||
func (e SessionNotFoundError) UserError() string {
|
||||
return e.Error()
|
||||
}
|
||||
|
||||
func (e SessionNotFoundError) Code() ErrorCode {
|
||||
return ErrorCodeEntityNotFound
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package domain_test
|
|||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
|
@ -101,3 +102,15 @@ func TestNewCreateSessionParams(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionNotFoundError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := domain.SessionNotFoundError{
|
||||
ServerKey: "pl151",
|
||||
}
|
||||
var _ domain.Error = err
|
||||
assert.Equal(t, fmt.Sprintf("session (ServerKey=%s) not found", err.ServerKey), err.Error())
|
||||
assert.Equal(t, err.Error(), err.UserError())
|
||||
assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package model
|
||||
|
||||
import "gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
|
||||
type Session struct {
|
||||
ServerKey string `json:"serverKey"`
|
||||
SID string `json:"sid"`
|
||||
} // @name Session
|
||||
|
||||
func NewSession(s domain.Session) Session {
|
||||
return Session{
|
||||
ServerKey: s.ServerKey,
|
||||
SID: s.SID,
|
||||
}
|
||||
}
|
||||
|
||||
type GetSessionResp struct {
|
||||
Data Session `json:"data"`
|
||||
} // @name GetSessionResp
|
||||
|
||||
func NewGetSessionResp(s domain.Session) GetSessionResp {
|
||||
return GetSessionResp{Data: NewSession(s)}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package model_test
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/router/rest/internal/model"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sess := domain.Session{
|
||||
ID: 1111,
|
||||
UserID: 111,
|
||||
ServerKey: "pl151",
|
||||
SID: base64.StdEncoding.EncodeToString([]byte(uuid.NewString())),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
assertSession(t, sess, model.NewSession(sess))
|
||||
}
|
||||
|
||||
func TestNewGetSessionResp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sess := domain.Session{
|
||||
ID: 1111,
|
||||
UserID: 111,
|
||||
ServerKey: "pl151",
|
||||
SID: base64.StdEncoding.EncodeToString([]byte(uuid.NewString())),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
assertSession(t, sess, model.NewGetSessionResp(sess).Data)
|
||||
}
|
||||
|
||||
func assertSession(tb testing.TB, du domain.Session, ru model.Session) {
|
||||
tb.Helper()
|
||||
|
||||
assert.Equal(tb, du.ServerKey, ru.ServerKey)
|
||||
assert.Equal(tb, du.SID, ru.SID)
|
||||
}
|
|
@ -71,8 +71,9 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
|
|||
|
||||
authMw := authMiddleware(cfg.APIKeyVerifier)
|
||||
|
||||
r.With(authMw).Get("/user", uh.getAuthenticated)
|
||||
r.With(authMw).Get("/user", uh.getCurrent)
|
||||
r.With(authMw).Put("/user/sessions/{serverKey}", sh.createOrUpdate)
|
||||
r.With(authMw).Get("/user/sessions/{serverKey}", sh.getCurrentUser)
|
||||
})
|
||||
|
||||
return router
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/router/rest/internal/model"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
|
@ -16,6 +17,7 @@ const (
|
|||
//counterfeiter:generate -o internal/mock/session_service.gen.go . SessionService
|
||||
type SessionService interface {
|
||||
CreateOrUpdate(ctx context.Context, params domain.CreateSessionParams) (domain.Session, error)
|
||||
Get(ctx context.Context, userID int64, serverKey string) (domain.Session, error)
|
||||
}
|
||||
|
||||
type sessionHandler struct {
|
||||
|
@ -28,8 +30,8 @@ type sessionHandler struct {
|
|||
// @Tags users,sessions
|
||||
// @Accept plain
|
||||
// @Success 204
|
||||
// @Success 400 {object} model.ErrorResp
|
||||
// @Success 401 {object} model.ErrorResp
|
||||
// @Failure 400 {object} model.ErrorResp
|
||||
// @Failure 401 {object} model.ErrorResp
|
||||
// @Failure 500 {object} model.ErrorResp
|
||||
// @Security ApiKeyAuth
|
||||
// @Param serverKey path string true "Server key"
|
||||
|
@ -60,3 +62,29 @@ func (h *sessionHandler) createOrUpdate(w http.ResponseWriter, r *http.Request)
|
|||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// @ID getCurrentUserSession
|
||||
// @Summary Get a session
|
||||
// @Description Get a session
|
||||
// @Tags users,sessions
|
||||
// @Success 200 {object} model.GetSessionResp
|
||||
// @Failure 400 {object} model.ErrorResp
|
||||
// @Failure 401 {object} model.ErrorResp
|
||||
// @Failure 404 {object} model.ErrorResp
|
||||
// @Failure 500 {object} model.ErrorResp
|
||||
// @Security ApiKeyAuth
|
||||
// @Param serverKey path string true "Server key"
|
||||
// @Router /user/sessions/{serverKey} [get]
|
||||
func (h *sessionHandler) getCurrentUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
chiCtx := chi.RouteContext(ctx)
|
||||
user, _ := userFromContext(ctx)
|
||||
|
||||
sess, err := h.svc.Get(ctx, user.ID, chiCtx.URLParam("serverKey"))
|
||||
if err != nil {
|
||||
renderErr(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
renderJSON(w, http.StatusOK, model.NewGetSessionResp(sess))
|
||||
}
|
||||
|
|
|
@ -34,19 +34,11 @@ func TestSession_createOrUpdate(t *testing.T) {
|
|||
{
|
||||
name: "OK",
|
||||
setup: func(apiKeySvc *mock.FakeAPIKeyVerifier, sessionSvc *mock.FakeSessionService) {
|
||||
apiKeySvc.VerifyCalls(func(ctx context.Context, key string) (domain.User, error) {
|
||||
if key != apiKey {
|
||||
return domain.User{}, domain.APIKeyNotFoundError{
|
||||
Key: key,
|
||||
}
|
||||
}
|
||||
|
||||
return domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
})
|
||||
apiKeySvc.VerifyReturns(domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil)
|
||||
sessionSvc.CreateOrUpdateReturns(domain.Session{}, nil)
|
||||
},
|
||||
apiKey: apiKey,
|
||||
|
@ -57,19 +49,11 @@ func TestSession_createOrUpdate(t *testing.T) {
|
|||
{
|
||||
name: "ERR: len(serverKey) > 10",
|
||||
setup: func(apiKeySvc *mock.FakeAPIKeyVerifier, sessionSvc *mock.FakeSessionService) {
|
||||
apiKeySvc.VerifyCalls(func(ctx context.Context, key string) (domain.User, error) {
|
||||
if key != apiKey {
|
||||
return domain.User{}, domain.APIKeyNotFoundError{
|
||||
Key: key,
|
||||
}
|
||||
}
|
||||
|
||||
return domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
})
|
||||
apiKeySvc.VerifyReturns(domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil)
|
||||
},
|
||||
apiKey: apiKey,
|
||||
serverKey: "012345678890",
|
||||
|
@ -86,19 +70,11 @@ func TestSession_createOrUpdate(t *testing.T) {
|
|||
{
|
||||
name: "ERR: SID is required",
|
||||
setup: func(apiKeySvc *mock.FakeAPIKeyVerifier, sessionSvc *mock.FakeSessionService) {
|
||||
apiKeySvc.VerifyCalls(func(ctx context.Context, key string) (domain.User, error) {
|
||||
if key != apiKey {
|
||||
return domain.User{}, domain.APIKeyNotFoundError{
|
||||
Key: key,
|
||||
}
|
||||
}
|
||||
|
||||
return domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
})
|
||||
apiKeySvc.VerifyReturns(domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil)
|
||||
},
|
||||
apiKey: apiKey,
|
||||
serverKey: "pl151",
|
||||
|
@ -115,19 +91,11 @@ func TestSession_createOrUpdate(t *testing.T) {
|
|||
{
|
||||
name: "ERR: SID is not a valid base64",
|
||||
setup: func(apiKeySvc *mock.FakeAPIKeyVerifier, sessionSvc *mock.FakeSessionService) {
|
||||
apiKeySvc.VerifyCalls(func(ctx context.Context, key string) (domain.User, error) {
|
||||
if key != apiKey {
|
||||
return domain.User{}, domain.APIKeyNotFoundError{
|
||||
Key: key,
|
||||
}
|
||||
}
|
||||
|
||||
return domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
})
|
||||
apiKeySvc.VerifyReturns(domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil)
|
||||
},
|
||||
apiKey: apiKey,
|
||||
serverKey: "pl151",
|
||||
|
@ -216,3 +184,139 @@ func TestSession_createOrUpdate(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSession_getCurrentUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Now()
|
||||
apiKey := uuid.NewString()
|
||||
sid := base64.StdEncoding.EncodeToString([]byte(uuid.NewString()))
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*mock.FakeAPIKeyVerifier, *mock.FakeSessionService)
|
||||
apiKey string
|
||||
serverKey string
|
||||
expectedStatus int
|
||||
target any
|
||||
expectedResponse any
|
||||
}{
|
||||
{
|
||||
name: "OK",
|
||||
setup: func(apiKeySvc *mock.FakeAPIKeyVerifier, sessionSvc *mock.FakeSessionService) {
|
||||
apiKeySvc.VerifyReturns(domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil)
|
||||
sessionSvc.GetCalls(func(ctx context.Context, userID int64, serverKey string) (domain.Session, error) {
|
||||
return domain.Session{
|
||||
ID: 111,
|
||||
UserID: userID,
|
||||
ServerKey: serverKey,
|
||||
SID: sid,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}, nil
|
||||
})
|
||||
},
|
||||
apiKey: apiKey,
|
||||
serverKey: "pl151",
|
||||
expectedStatus: http.StatusOK,
|
||||
target: &model.GetSessionResp{},
|
||||
expectedResponse: &model.GetSessionResp{
|
||||
Data: model.Session{
|
||||
ServerKey: "pl151",
|
||||
SID: sid,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: session not found",
|
||||
setup: func(apiKeySvc *mock.FakeAPIKeyVerifier, sessionSvc *mock.FakeSessionService) {
|
||||
apiKeySvc.VerifyReturns(domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil)
|
||||
sessionSvc.GetCalls(func(ctx context.Context, userID int64, serverKey string) (domain.Session, error) {
|
||||
return domain.Session{}, domain.SessionNotFoundError{
|
||||
ServerKey: serverKey,
|
||||
}
|
||||
})
|
||||
},
|
||||
apiKey: apiKey,
|
||||
expectedStatus: http.StatusNotFound,
|
||||
serverKey: "pl151",
|
||||
target: &model.ErrorResp{},
|
||||
expectedResponse: &model.ErrorResp{
|
||||
Error: model.APIError{
|
||||
Code: domain.ErrorCodeEntityNotFound.String(),
|
||||
Message: "session (ServerKey=pl151) not found",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: apiKey == \"\"",
|
||||
setup: func(apiKeySvc *mock.FakeAPIKeyVerifier, sessionSvc *mock.FakeSessionService) {},
|
||||
apiKey: "",
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
serverKey: "pl151",
|
||||
target: &model.ErrorResp{},
|
||||
expectedResponse: &model.ErrorResp{
|
||||
Error: model.APIError{
|
||||
Code: "unauthorized",
|
||||
Message: "invalid API key",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: unexpected API key",
|
||||
setup: func(apiKeySvc *mock.FakeAPIKeyVerifier, sessionSvc *mock.FakeSessionService) {
|
||||
apiKeySvc.VerifyCalls(func(ctx context.Context, key string) (domain.User, error) {
|
||||
if key != apiKey {
|
||||
return domain.User{}, domain.APIKeyNotFoundError{
|
||||
Key: key,
|
||||
}
|
||||
}
|
||||
|
||||
return domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
})
|
||||
},
|
||||
apiKey: uuid.NewString(),
|
||||
serverKey: "pl151",
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
target: &model.ErrorResp{},
|
||||
expectedResponse: &model.ErrorResp{
|
||||
Error: model.APIError{
|
||||
Code: "unauthorized",
|
||||
Message: "invalid API key",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
apiKeySvc := &mock.FakeAPIKeyVerifier{}
|
||||
sessionSvc := &mock.FakeSessionService{}
|
||||
tt.setup(apiKeySvc, sessionSvc)
|
||||
|
||||
router := rest.NewRouter(rest.RouterConfig{
|
||||
APIKeyVerifier: apiKeySvc,
|
||||
SessionService: sessionSvc,
|
||||
})
|
||||
|
||||
resp := doRequest(router, http.MethodGet, "/v1/user/sessions/"+tt.serverKey, tt.apiKey, nil)
|
||||
defer resp.Body.Close()
|
||||
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,17 +9,17 @@ import (
|
|||
type userHandler struct {
|
||||
}
|
||||
|
||||
// @ID getAuthenticatedUser
|
||||
// @ID getCurrentUser
|
||||
// @Summary Get the authenticated user
|
||||
// @Description Get the authenticated user
|
||||
// @Tags users
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.GetUserResp
|
||||
// @Success 401 {object} model.ErrorResp
|
||||
// @Failure 401 {object} model.ErrorResp
|
||||
// @Failure 500 {object} model.ErrorResp
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /user [get]
|
||||
func (h *userHandler) getAuthenticated(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *userHandler) getCurrent(w http.ResponseWriter, r *http.Request) {
|
||||
u, _ := userFromContext(r.Context())
|
||||
renderJSON(w, http.StatusOK, model.NewGetUserResp(u))
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ func NewAPIKey(repo APIKeyRepository, userSvc UserGetter) *APIKey {
|
|||
return &APIKey{repo: repo, userSvc: userSvc}
|
||||
}
|
||||
|
||||
func (a *APIKey) Create(ctx context.Context, userID int64) (domain.APIKey, error) {
|
||||
func (a *APIKey) Create(ctx context.Context, name string, userID int64) (domain.APIKey, error) {
|
||||
user, err := a.userSvc.Get(ctx, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.UserNotFoundError{ID: userID}) {
|
||||
|
@ -43,7 +43,7 @@ func (a *APIKey) Create(ctx context.Context, userID int64) (domain.APIKey, error
|
|||
return domain.APIKey{}, fmt.Errorf("couldn't generate api key: %w", err)
|
||||
}
|
||||
|
||||
params, err := domain.NewCreateAPIKeyParams(key.String(), user.ID)
|
||||
params, err := domain.NewCreateAPIKeyParams(name, key.String(), user.ID)
|
||||
if err != nil {
|
||||
return domain.APIKey{}, err
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ func TestAPIKey_Create(t *testing.T) {
|
|||
apiKeyRepo.CreateCalls(func(ctx context.Context, params domain.CreateAPIKeyParams) (domain.APIKey, error) {
|
||||
return domain.APIKey{
|
||||
ID: 1,
|
||||
Name: params.Name(),
|
||||
Key: params.Key(),
|
||||
UserID: params.UserID(),
|
||||
CreatedAt: time.Now(),
|
||||
|
@ -32,9 +33,11 @@ func TestAPIKey_Create(t *testing.T) {
|
|||
user := domain.User{ID: 1234}
|
||||
userSvc.GetReturns(user, nil)
|
||||
|
||||
ak, err := service.NewAPIKey(apiKeyRepo, userSvc).Create(context.Background(), user.ID)
|
||||
name := uuid.NewString()
|
||||
ak, err := service.NewAPIKey(apiKeyRepo, userSvc).Create(context.Background(), name, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, ak.ID, int64(0))
|
||||
assert.Equal(t, name, ak.Name)
|
||||
_, err = uuid.Parse(ak.Key)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.ID, ak.UserID)
|
||||
|
@ -48,7 +51,7 @@ func TestAPIKey_Create(t *testing.T) {
|
|||
var userID int64 = 1234
|
||||
userSvc.GetReturns(domain.User{}, domain.UserNotFoundError{ID: userID})
|
||||
|
||||
ak, err := service.NewAPIKey(nil, userSvc).Create(context.Background(), userID)
|
||||
ak, err := service.NewAPIKey(nil, userSvc).Create(context.Background(), uuid.NewString(), userID)
|
||||
assert.ErrorIs(t, err, domain.UserDoesNotExistError{ID: userID})
|
||||
assert.Zero(t, ak)
|
||||
})
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
//counterfeiter:generate -o internal/mock/session_repository.gen.go . SessionRepository
|
||||
type SessionRepository interface {
|
||||
CreateOrUpdate(ctx context.Context, params domain.CreateSessionParams) (domain.Session, error)
|
||||
Get(ctx context.Context, userID int64, serverKey string) (domain.Session, error)
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
|
@ -37,3 +38,11 @@ func (s *Session) CreateOrUpdate(ctx context.Context, params domain.CreateSessio
|
|||
|
||||
return sess, nil
|
||||
}
|
||||
|
||||
func (s *Session) Get(ctx context.Context, userID int64, serverKey string) (domain.Session, error) {
|
||||
sess, err := s.repo.Get(ctx, userID, serverKey)
|
||||
if err != nil {
|
||||
return domain.Session{}, fmt.Errorf("SessionRepository.Get: %w", err)
|
||||
}
|
||||
return sess, nil
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ go 1.19
|
|||
require (
|
||||
github.com/golangci/golangci-lint v1.50.1
|
||||
github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0
|
||||
github.com/swaggo/swag v1.8.8
|
||||
github.com/swaggo/swag v1.8.9
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
|
@ -542,8 +542,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
|
|||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
|
||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/swaggo/swag v1.8.8 h1:/GgJmrJ8/c0z4R4hoEPZ5UeEhVGdvsII4JbVDLbR7Xc=
|
||||
github.com/swaggo/swag v1.8.8/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
|
||||
github.com/swaggo/swag v1.8.9 h1:kHtaBe/Ob9AZzAANfcn5c6RyCke9gG9QpH0jky0I/sA=
|
||||
github.com/swaggo/swag v1.8.9/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
|
||||
github.com/tdakkota/asciicheck v0.1.1 h1:PKzG7JUTUmVspQTDqtkX9eSiLGossXTybutHwTXuO0A=
|
||||
github.com/tdakkota/asciicheck v0.1.1/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
||||
github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
|
||||
|
|
|
@ -31,7 +31,7 @@ spec:
|
|||
- name: DB_MAX_IDLE_CONNECTIONS
|
||||
value: "5"
|
||||
- name: API_SWAGGER_ENABLED
|
||||
value: "true"
|
||||
value: "false"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /_meta/livez
|
||||
|
@ -46,11 +46,11 @@ spec:
|
|||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 100Mi
|
||||
cpu: 50m
|
||||
memory: 50Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 300Mi
|
||||
cpu: 100m
|
||||
memory: 150Mi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: sessions-api-deployment
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: sessions-api
|
||||
image: sessions
|
||||
env:
|
||||
- name: APP_MODE
|
||||
value: development
|
||||
- name: DB_DSN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: sessions-secret
|
||||
key: db-dsn
|
||||
- name: DB_MAX_OPEN_CONNECTIONS
|
||||
value: "10"
|
||||
- name: DB_MAX_IDLE_CONNECTIONS
|
||||
value: "5"
|
||||
- name: API_SWAGGER_ENABLED
|
||||
value: "true"
|
||||
- name: API_CORS_ENABLED
|
||||
value: "true"
|
||||
- name: API_CORS_ALLOWED_ORIGINS
|
||||
value: "*"
|
|
@ -4,3 +4,5 @@ nameSuffix: -dev
|
|||
resources:
|
||||
- secret.yml
|
||||
- ../../base
|
||||
patchesStrategicMerge:
|
||||
- api.yml
|
|
@ -0,0 +1,24 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: sessions-api-deployment
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: sessions-api
|
||||
image: sessions
|
||||
env:
|
||||
- name: APP_MODE
|
||||
value: production
|
||||
- name: DB_DSN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: sessions-secret
|
||||
key: db-dsn
|
||||
- name: DB_MAX_OPEN_CONNECTIONS
|
||||
value: "5"
|
||||
- name: DB_MAX_IDLE_CONNECTIONS
|
||||
value: "5"
|
||||
- name: API_SWAGGER_ENABLED
|
||||
value: "false"
|
|
@ -0,0 +1,20 @@
|
|||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: sessions-api-ingress
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: "web"
|
||||
traefik.ingress.kubernetes.io/router.tls: "false"
|
||||
traefik.ingress.kubernetes.io/router.middlewares: kube-system-cors-sessions@kubernetescrd,kube-system-compress@kubernetescrd
|
||||
spec:
|
||||
rules:
|
||||
- host: sessions.tribalwarshelp.com
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: sessions-api-service
|
||||
port:
|
||||
number: 9234
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: sessions-migrations-job
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: sessions-migrations
|
||||
image: sessions
|
||||
env:
|
||||
- name: APP_MODE
|
||||
value: production
|
||||
- name: DB_MAX_OPEN_CONNECTIONS
|
||||
value: "1"
|
||||
- name: DB_MAX_IDLE_CONNECTIONS
|
||||
value: "1"
|
||||
- name: DB_DSN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: sessions-secret
|
||||
key: db-dsn
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: twhelp
|
||||
resources:
|
||||
- ../../base
|
||||
- ingress.yml
|
||||
patchesStrategicMerge:
|
||||
- api.yml
|
||||
- jobs.yml
|
|
@ -6,6 +6,10 @@ build:
|
|||
template: latest
|
||||
artifacts:
|
||||
- image: sessions
|
||||
hooks:
|
||||
before:
|
||||
- command: [ "sh", "-c", "make generate" ]
|
||||
os: [ darwin, linux ]
|
||||
context: .
|
||||
docker:
|
||||
dockerfile: ./build/docker/sessions/dev/Dockerfile
|
||||
|
|
Loading…
Reference in New Issue