Compare commits

..

17 Commits

Author SHA1 Message Date
Renovate a7d2b5c9f2 chore(deps): update module github.com/swaggo/swag to v1.8.9
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing Details
2022-12-16 00:02:30 +00:00
Dawid Wysokiński 5777aee059 chore: update dependencies (#26)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #26
2022-12-11 09:00:55 +00:00
Dawid Wysokiński c71f24c00d
chore: update README.md
continuous-integration/drone/push Build is passing Details
2022-12-11 09:51:49 +01:00
Dawid Wysokiński 900e7abc58
chore: update README.md
continuous-integration/drone/push Build is passing Details
2022-12-09 06:38:43 +01:00
Dawid Wysokiński 0d6ee87cae refactor: bundb - simplify newDB (#23)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #23
2022-12-07 06:00:20 +00:00
Dawid Wysokiński a67b911e77
chore: add LICENSE
continuous-integration/drone/push Build is passing Details
2022-12-07 06:57:48 +01:00
Dawid Wysokiński b8bd8cd4df chore: README.md (#22)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #22
2022-12-07 05:49:08 +00:00
renovate d81f7e4945 chore(deps): update golang docker tag to v1.19.4 (#21)
continuous-integration/drone/push Build is passing Details
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| golang | stage | patch | `1.19.3-alpine3.16` -> `1.19.4-alpine3.16` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNC4yMy4wIiwidXBkYXRlZEluVmVyIjoiMzQuMjMuMCJ9-->

Co-authored-by: Renovate <renovate@dwysokinski.me>
Reviewed-on: #21
Co-authored-by: renovate <renovate@noreply.localhost>
Co-committed-by: renovate <renovate@noreply.localhost>
2022-12-07 04:39:00 +00:00
Dawid Wysokiński 3adb5f910a feat: govulncheck (#19)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #19
2022-12-04 07:54:54 +00:00
Dawid Wysokiński d7176066f4
chore(deps): update module go.uber.org/zap to v1.24.0
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-12-02 06:32:32 +01:00
Dawid Wysokiński 847ac51b38 feat: session name (#18)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #18
2022-12-02 05:29:11 +00:00
Dawid Wysokiński 5f144a9fd2
fix: migrations table is already locked
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-11-27 09:37:07 +01:00
Dawid Wysokiński 026db8e50f
chore: api - update resources
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-11-27 09:28:49 +01:00
Dawid Wysokiński 037e43b299
chore: update .drone.yml
continuous-integration/drone/push Build is passing Details
2022-11-27 09:23:54 +01:00
Dawid Wysokiński d24d43bfd2
chore: prod env setup
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-11-27 09:11:30 +01:00
Dawid Wysokiński 10cb5b83b1
refactor: enable cors - dev env + swagger is disabled by default
continuous-integration/drone/push Build is passing Details
2022-11-26 08:14:56 +01:00
Dawid Wysokiński 786cc60e38 feat: add a new REST endpoint - GET /api/v1/user/sessions/:server (#14)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #14
2022-11-25 05:55:31 +00:00
43 changed files with 945 additions and 104 deletions

View File

@ -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
...

21
LICENSE Normal file
View File

@ -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.

87
README.md Normal file
View File

@ -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)

View File

@ -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"]

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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"`
}

BIN
docs/migration.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

8
go.mod
View File

@ -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
View File

@ -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=

View File

@ -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,
)

View File

@ -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)
}

View File

@ -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,

View File

@ -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)

View File

@ -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
})
}

View File

@ -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) {

View File

@ -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)
})
}
})
}

View File

@ -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

View File

@ -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)
})

View File

@ -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 {

View File

@ -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)

View File

@ -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
}

View File

@ -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())
}

View File

@ -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)}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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))
}

View File

@ -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)
})
}
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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)
})

View File

@ -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
}

View File

@ -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 (

View File

@ -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=

View File

@ -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

28
k8s/overlays/dev/api.yml Normal file
View File

@ -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: "*"

View File

@ -4,3 +4,5 @@ nameSuffix: -dev
resources:
- secret.yml
- ../../base
patchesStrategicMerge:
- api.yml

24
k8s/overlays/prod/api.yml Normal file
View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: twhelp
resources:
- ../../base
- ingress.yml
patchesStrategicMerge:
- api.yml
- jobs.yml

View File

@ -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