This commit is contained in:
commit
cc798de6bc
796 changed files with 123103 additions and 346605 deletions
44
.gitea/workflows/build-image.yaml
Normal file
44
.gitea/workflows/build-image.yaml
Normal file
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
|
||||
env:
|
||||
DOCKER_REGISTRY: source.toby3d.me
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://gitea.com/actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: https://gitea.com/docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker BuildX
|
||||
uses: https://gitea.com/docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to registry
|
||||
uses: https://gitea.com/docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ gitea.repository_owner }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: https://gitea.com/docker/build-push-action@v4
|
||||
env:
|
||||
ACTIONS_RUNTIME_TOKEN: "" # See https://gitea.com/gitea/act_runner/issues/119
|
||||
with:
|
||||
context: .
|
||||
file: ./build/Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.DOCKER_REGISTRY }}/${{ gitea.repository }}:${{ gitea.ref_name }}
|
|
@ -59,6 +59,15 @@ linters-settings:
|
|||
- "i int"
|
||||
- "r *http.Request"
|
||||
- "w http.ResponseWriter"
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
allow:
|
||||
- $gostd
|
||||
- github.com/toby3d
|
||||
- gitlab.com/toby3d
|
||||
- source.toby3d.me
|
||||
- golang.org
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
|
|
28
build/Dockerfile
Normal file
28
build/Dockerfile
Normal file
|
@ -0,0 +1,28 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# docker build --rm -f build/Dockerfile -t source.toby3d.me/toby3d/auth .
|
||||
|
||||
# Build
|
||||
FROM golang:alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOFLAGS="-mod=vendor -buildvcs=true"
|
||||
|
||||
COPY go.mod go.sum *.go ./
|
||||
COPY internal ./internal/
|
||||
COPY vendor ./vendor/
|
||||
COPY web ./web/
|
||||
|
||||
RUN go build -o ./auth
|
||||
|
||||
# Run
|
||||
FROM scratch
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY --from=builder /app/auth /auth
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["/auth"]
|
|
@ -39,43 +39,65 @@ func init() {
|
|||
}
|
||||
|
||||
var messageKeyToIndex = map[string]int{
|
||||
"Allow": 4,
|
||||
"%sProof of Key Code Exchange%s is a mechanism that protects against attackers in the middle hijacking your application's authentication process. You can still authorize this application without this protection, but you must independently verify the security of this connection. If you have any doubts - stop the process and contact the developers.": 4,
|
||||
"Allow": 8,
|
||||
"Authorize %s": 0,
|
||||
"Authorize application": 1,
|
||||
"Choose your scopes": 2,
|
||||
"Deny": 3,
|
||||
"Error": 6,
|
||||
"How do I fix it?": 7,
|
||||
"Recipient": 10,
|
||||
"Resource": 11,
|
||||
"Send": 12,
|
||||
"Sign In": 8,
|
||||
"TicketAuth": 9,
|
||||
"version": 5,
|
||||
"Deny": 7,
|
||||
"Error": 10,
|
||||
"How do I fix it?": 11,
|
||||
"No scopes is requested: the application will only get your profile URL.": 6,
|
||||
"Recipient": 14,
|
||||
"Resource": 15,
|
||||
"Scopes": 5,
|
||||
"Send": 16,
|
||||
"Sign In": 12,
|
||||
"This client does not use %sPKCE%s!": 3,
|
||||
"This client uses %sPKCE%s with the %s%s%s method.": 2,
|
||||
"TicketAuth": 13,
|
||||
"You will be redirected to %s%s%s": 9,
|
||||
}
|
||||
|
||||
var enIndex = []uint32{ // 14 elements
|
||||
0x00000000, 0x00000010, 0x00000026, 0x00000039,
|
||||
0x0000003e, 0x00000044, 0x0000004c, 0x00000052,
|
||||
0x00000063, 0x0000006b, 0x00000076, 0x00000080,
|
||||
0x00000089, 0x0000008e,
|
||||
} // Size: 80 bytes
|
||||
var enIndex = []uint32{ // 18 elements
|
||||
0x00000000, 0x00000010, 0x00000026, 0x00000067,
|
||||
0x00000090, 0x000001f3, 0x000001fa, 0x00000242,
|
||||
0x00000247, 0x0000024d, 0x00000277, 0x0000027d,
|
||||
0x0000028e, 0x00000296, 0x000002a1, 0x000002ab,
|
||||
0x000002b4, 0x000002b9,
|
||||
} // Size: 96 bytes
|
||||
|
||||
const enData string = "" + // Size: 142 bytes
|
||||
"\x02Authorize %[1]s\x02Authorize application\x02Choose your scopes\x02De" +
|
||||
"ny\x02Allow\x02version\x02Error\x02How do I fix it?\x02Sign In\x02Ticket" +
|
||||
"Auth\x02Recipient\x02Resource\x02Send"
|
||||
const enData string = "" + // Size: 697 bytes
|
||||
"\x02Authorize %[1]s\x02Authorize application\x02This client uses %[1]sPK" +
|
||||
"CE%[2]s with the %[3]s%[4]s%[5]s method.\x02This client does not use %[1" +
|
||||
"]sPKCE%[2]s!\x02%[1]sProof of Key Code Exchange%[2]s is a mechanism that" +
|
||||
" protects against attackers in the middle hijacking your application's a" +
|
||||
"uthentication process. You can still authorize this application without " +
|
||||
"this protection, but you must independently verify the security of this " +
|
||||
"connection. If you have any doubts - stop the process and contact the d" +
|
||||
"evelopers.\x02Scopes\x02No scopes is requested: the application will onl" +
|
||||
"y get your profile URL.\x02Deny\x02Allow\x02You will be redirected to %[" +
|
||||
"1]s%[2]s%[3]s\x02Error\x02How do I fix it?\x02Sign In\x02TicketAuth\x02R" +
|
||||
"ecipient\x02Resource\x02Send"
|
||||
|
||||
var ruIndex = []uint32{ // 14 elements
|
||||
0x00000000, 0x0000001f, 0x0000004d, 0x0000008e,
|
||||
0x0000009f, 0x000000b2, 0x000000bf, 0x000000cc,
|
||||
0x000000ee, 0x000000f9, 0x00000104, 0x00000119,
|
||||
0x00000126, 0x00000139,
|
||||
} // Size: 80 bytes
|
||||
var ruIndex = []uint32{ // 18 elements
|
||||
0x00000000, 0x0000001f, 0x0000004d, 0x000000a1,
|
||||
0x000000d8, 0x00000343, 0x00000352, 0x000003e9,
|
||||
0x000003fa, 0x0000040d, 0x00000451, 0x0000045e,
|
||||
0x00000480, 0x0000048b, 0x00000496, 0x000004ab,
|
||||
0x000004b8, 0x000004cb,
|
||||
} // Size: 96 bytes
|
||||
|
||||
const ruData string = "" + // Size: 313 bytes
|
||||
"\x02Авторизовать %[1]s\x02Авторизовать приложение\x02Выбери предоставляе" +
|
||||
"мые разрешения\x02Отказать\x02Разрешить\x02версия\x02Ошибка\x02Как испр" +
|
||||
"авить это?\x02Войти\x02TicketAuth\x02Получатель\x02Ресурс\x02Отправить"
|
||||
const ruData string = "" + // Size: 1227 bytes
|
||||
"\x02Авторизовать %[1]s\x02Авторизовать приложение\x02Клиент использует %" +
|
||||
"[1]sPKCE%[2]s с методом %[3]s%[4]s%[5]s.\x02Клиент не использует %[1]sPK" +
|
||||
"CE%[2]s!\x02%[1]sProof of Key Code Exchange%[2]s это механизм, защищающи" +
|
||||
"й от злоумышленников, перехватывающих процесс аутентификации вашего при" +
|
||||
"ложения. Вы можете авторизовать данное приложение и без этой защиты, но" +
|
||||
" при этом вы должны самостоятельно проверить безопасность этого соединен" +
|
||||
"ия. Если у вас есть сомнения - прервите процесс и свяжитесь с разработч" +
|
||||
"иками.\x02Области\x02Никакие разрешения не запрашиваются: приложение по" +
|
||||
"лучит только URL вашего профиля.\x02Отказать\x02Разрешить\x02Вы будете " +
|
||||
"перенаправлены на %[1]s%[2]s%[3]s\x02Ошибка\x02Как исправить это?\x02Во" +
|
||||
"йти\x02TicketAuth\x02Получатель\x02Ресурс\x02Отправить"
|
||||
|
||||
// Total table size 615 bytes (0KiB); checksum: 66FB60EC
|
||||
// Total table size 2116 bytes (2KiB); checksum: 848DD07E
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# auth [![Build Status](https://drone.toby3d.me/api/badges/toby3d/auth/status.svg)](https://drone.toby3d.me/toby3d/auth)
|
||||
# auth
|
||||
|
||||
> Personal [IndieAuth](https://indieauth.net/source/) server.
|
||||
|
||||
|
|
65
go.mod
65
go.mod
|
@ -1,60 +1,57 @@
|
|||
module source.toby3d.me/toby3d/auth
|
||||
|
||||
go 1.19
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0
|
||||
github.com/brianvoe/gofakeit/v6 v6.20.2
|
||||
github.com/caarlos0/env/v7 v7.1.0
|
||||
github.com/go-logfmt/logfmt v0.6.0
|
||||
github.com/goccy/go-json v0.10.1
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/brianvoe/gofakeit/v6 v6.24.0
|
||||
github.com/caarlos0/env/v9 v9.0.0
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/jmoiron/sqlx v1.3.5
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.8
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.16
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
|
||||
github.com/valyala/fasttemplate v1.2.2
|
||||
github.com/valyala/quicktemplate v1.7.0
|
||||
go.etcd.io/bbolt v1.3.7
|
||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0
|
||||
golang.org/x/text v0.8.0
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2
|
||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317
|
||||
modernc.org/sqlite v1.21.0
|
||||
source.toby3d.me/toby3d/form v0.3.0
|
||||
go.etcd.io/bbolt v1.3.8
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||
golang.org/x/text v0.13.0
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028
|
||||
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a
|
||||
modernc.org/sqlite v1.27.0
|
||||
source.toby3d.me/toby3d/form v0.4.0
|
||||
willnorris.com/go/microformats v1.2.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.16.3 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.4 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lib/pq v1.10.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.45.0 // indirect
|
||||
go4.org/intern v0.0.0-20230205224052-192e9f60865c // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230221090011-e4bae7ad2296 // indirect
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
golang.org/x/mod v0.9.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/tools v0.7.0 // indirect
|
||||
go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/mod v0.13.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/tools v0.14.0 // indirect
|
||||
lukechampine.com/uint128 v1.3.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.3 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/cc/v3 v3.41.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.15 // indirect
|
||||
modernc.org/libc v1.29.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
||||
|
|
175
go.sum
175
go.sum
|
@ -2,122 +2,123 @@ github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20O
|
|||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/brianvoe/gofakeit/v6 v6.20.2 h1:FLloufuC7NcbHqDzVQ42CG9AKryS1gAGCRt8nQRsW+Y=
|
||||
github.com/brianvoe/gofakeit/v6 v6.20.2/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
|
||||
github.com/caarlos0/env/v7 v7.1.0 h1:9lzTF5amyQeWHZzuZeKlCb5FWSUxpG1js43mhbY8ozg=
|
||||
github.com/caarlos0/env/v7 v7.1.0/go.mod h1:LPPWniDUq4JaO6Q41vtlyikhMknqymCLBw0eX4dcH1E=
|
||||
github.com/brianvoe/gofakeit/v6 v6.24.0 h1:74yq7RRz/noddscZHRS2T84oHZisW9muwbb8sRnU52A=
|
||||
github.com/brianvoe/gofakeit/v6 v6.24.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
|
||||
github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc=
|
||||
github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.1 h1:lEs5Ob+oOG/Ze199njvzHbhn6p9T+h64F5hRj69iTTo=
|
||||
github.com/goccy/go-json v0.10.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
|
||||
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
|
||||
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.8 h1:jCFT8oc0hEDVjgUgsBy1F9cbjsjAVZSXNi7JaU9HR/Q=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.8/go.mod h1:zLxnyv9rTlEvOUHbc48FAfIL8iYu2hHvIRaTFGc8mT0=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.16 h1:TuH3dBkYTy2giQg/9D8f20znS3JtMRuQJ372boS3lWk=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.16/go.mod h1:jBHyESp4e7QxfERM0UKkQ80/94paqNIEcdEfiUYz5zE=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
|
||||
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
|
||||
github.com/valyala/fasthttp v1.45.0 h1:zPkkzpIn8tdHZUrVa6PzYd0i5verqiPSkgTd3bSUcpA=
|
||||
github.com/valyala/fasthttp v1.45.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM=
|
||||
github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
|
||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
|
||||
go4.org/intern v0.0.0-20230205224052-192e9f60865c h1:b8WZ7Ja8nKegYxfwDLLwT00ZKv4lXAQrw8LYPK+cHSI=
|
||||
go4.org/intern v0.0.0-20230205224052-192e9f60865c/go.mod h1:RJ0SVrOMpxLhgb5noIV+09zI1RsRlMsbUcSxpWHqbrE=
|
||||
go4.org/intern v0.0.0-20230525184215-6c62f75575cb h1:ae7kzL5Cfdmcecbh22ll7lYP3iuUdnfnhiPcSaDgH/8=
|
||||
go4.org/intern v0.0.0-20230525184215-6c62f75575cb/go.mod h1:Ycrt6raEcnF5FTsLiLKkhBTO6DPX3RCUCUVnks3gFJU=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230204201903-c31fa085b70e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230221090011-e4bae7ad2296 h1:QJ/xcIANMLApehfgPCHnfK1hZiaMmbaTVmPv7DAoTbo=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230221090011-e4bae7ad2296/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
|
||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
|
||||
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -126,56 +127,74 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
|
||||
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317 h1:U2fwK6P2EqmopP/hFLTOAjWTki0qgd4GMJn5X8wOleU=
|
||||
inet.af/netaddr v0.0.0-20220811202034-502d2d690317/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
|
||||
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a h1:1XCVEdxrvL6c0TGOhecLuB7U9zYNdxZEjvOqJreKZiM=
|
||||
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a/go.mod h1:e83i32mAQOW1LAqEIweALsuK2Uw4mhQadA5r7b0Wobo=
|
||||
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
|
||||
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
|
||||
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
|
||||
modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
|
||||
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
|
||||
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs=
|
||||
modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.21.0 h1:4aP4MdUf15i3R3M2mx6Q90WHKz3nZLoz96zlB6tNdow=
|
||||
modernc.org/sqlite v1.21.0/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws=
|
||||
modernc.org/sqlite v1.27.0 h1:MpKAHoyYB7xqcwnUwkuD+npwEa0fojF0B5QRbN+auJ8=
|
||||
modernc.org/sqlite v1.27.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
|
||||
source.toby3d.me/toby3d/form v0.3.0 h1:kI8apdFeVr+koqTTGVoIRiR5NMqjrhCJlajYlu+1bVw=
|
||||
source.toby3d.me/toby3d/form v0.3.0/go.mod h1:drlHMC+j/gb5zsttCSwx8qcYsbaRW+wFfE8bK6y+oeY=
|
||||
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
||||
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
|
||||
source.toby3d.me/toby3d/form v0.4.0 h1:p4erlFQZpWi64oHQVYsNe8FKT75ZwnExELk69ZDoQO8=
|
||||
source.toby3d.me/toby3d/form v0.4.0/go.mod h1:drlHMC+j/gb5zsttCSwx8qcYsbaRW+wFfE8bK6y+oeY=
|
||||
willnorris.com/go/microformats v1.2.0 h1:73pzJCLJM69kYE5qsLI9OOC/7sImNVOzya9EQ0+1wmM=
|
||||
willnorris.com/go/microformats v1.2.0/go.mod h1:RrlwCSvib4qz+JICKiN7rON4phzQ3HAT7j6s4O2cZj4=
|
||||
|
|
|
@ -23,16 +23,16 @@ type (
|
|||
NewHandlerOptions struct {
|
||||
Auth auth.UseCase
|
||||
Clients client.UseCase
|
||||
Config domain.Config
|
||||
Matcher language.Matcher
|
||||
Profiles profile.UseCase
|
||||
Config domain.Config
|
||||
}
|
||||
|
||||
Handler struct {
|
||||
clients client.UseCase
|
||||
config domain.Config
|
||||
matcher language.Matcher
|
||||
useCase auth.UseCase
|
||||
config domain.Config
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -45,7 +45,7 @@ func NewHandler(opts NewHandlerOptions) *Handler {
|
|||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Handler() http.Handler {
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
chain := middleware.Chain{
|
||||
middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||
Skipper: func(_ http.ResponseWriter, r *http.Request) bool {
|
||||
|
@ -68,8 +68,7 @@ func (h *Handler) Handler() http.Handler {
|
|||
Skipper: func(_ http.ResponseWriter, r *http.Request) bool {
|
||||
head, _ := urlutil.ShiftPath(r.URL.Path)
|
||||
|
||||
return r.Method != http.MethodPost ||
|
||||
head != "verify" ||
|
||||
return r.Method != http.MethodPost || head != "verify" ||
|
||||
r.PostFormValue("authorize") == "deny"
|
||||
},
|
||||
Validator: func(_ http.ResponseWriter, _ *http.Request, login, password string) (bool, error) {
|
||||
|
@ -84,31 +83,29 @@ func (h *Handler) Handler() http.Handler {
|
|||
}),
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
head, _ := urlutil.ShiftPath(r.URL.Path)
|
||||
head, _ := urlutil.ShiftPath(r.URL.Path)
|
||||
|
||||
switch r.Method {
|
||||
default:
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
case http.MethodGet, "":
|
||||
if head != "" {
|
||||
http.NotFound(w, r)
|
||||
switch r.Method {
|
||||
default:
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
case http.MethodGet, "":
|
||||
if head != "" {
|
||||
http.NotFound(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
chain.Handler(h.handleAuthorize).ServeHTTP(w, r)
|
||||
case http.MethodPost:
|
||||
switch head {
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
case "":
|
||||
chain.Handler(h.handleExchange).ServeHTTP(w, r)
|
||||
case "verify":
|
||||
chain.Handler(h.handleVerify).ServeHTTP(w, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
chain.Handler(h.handleAuthorize).ServeHTTP(w, r)
|
||||
case http.MethodPost:
|
||||
switch head {
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
case "":
|
||||
chain.Handler(h.handleExchange).ServeHTTP(w, r)
|
||||
case "verify":
|
||||
chain.Handler(h.handleVerify).ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleAuthorize(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -169,7 +166,7 @@ func (h *Handler) handleAuthorize(w http.ResponseWriter, r *http.Request) {
|
|||
Client: client,
|
||||
Me: &req.Me,
|
||||
RedirectURI: &req.RedirectURI,
|
||||
CodeChallengeMethod: *req.CodeChallengeMethod,
|
||||
CodeChallengeMethod: req.CodeChallengeMethod,
|
||||
ResponseType: req.ResponseType,
|
||||
CodeChallenge: req.CodeChallenge,
|
||||
State: req.State,
|
||||
|
@ -210,7 +207,7 @@ func (h *Handler) handleVerify(w http.ResponseWriter, r *http.Request) {
|
|||
ClientID: req.ClientID,
|
||||
Me: req.Me,
|
||||
RedirectURI: req.RedirectURI.URL,
|
||||
CodeChallengeMethod: *req.CodeChallengeMethod,
|
||||
CodeChallengeMethod: req.CodeChallengeMethod,
|
||||
Scope: req.Scope,
|
||||
CodeChallenge: req.CodeChallenge,
|
||||
})
|
||||
|
@ -271,18 +268,8 @@ func (h *Handler) handleExchange(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
var userInfo *AuthProfileResponse
|
||||
if profile != nil {
|
||||
userInfo = &AuthProfileResponse{
|
||||
Email: profile.GetEmail(),
|
||||
Photo: &domain.URL{URL: profile.GetPhoto()},
|
||||
URL: &domain.URL{URL: profile.GetURL()},
|
||||
Name: profile.GetName(),
|
||||
}
|
||||
}
|
||||
|
||||
_ = encoder.Encode(&AuthExchangeResponse{
|
||||
Me: *me,
|
||||
Profile: userInfo,
|
||||
Me: me.String(),
|
||||
Profile: NewAuthProfileResponse(profile),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ type (
|
|||
Me domain.Me `form:"me"`
|
||||
|
||||
// The hashing method used to calculate the code challenge.
|
||||
CodeChallengeMethod *domain.CodeChallengeMethod `form:"code_challenge_method,omitempty"`
|
||||
CodeChallengeMethod domain.CodeChallengeMethod `form:"code_challenge_method,omitempty"`
|
||||
|
||||
// Indicates to the authorization server that an authorization
|
||||
// code should be returned as the response.
|
||||
|
@ -46,16 +46,16 @@ type (
|
|||
}
|
||||
|
||||
AuthVerifyRequest struct {
|
||||
ClientID domain.ClientID `form:"client_id"`
|
||||
Me domain.Me `form:"me"`
|
||||
RedirectURI domain.URL `form:"redirect_uri"`
|
||||
CodeChallengeMethod *domain.CodeChallengeMethod `form:"code_challenge_method,omitempty"`
|
||||
ResponseType domain.ResponseType `form:"response_type"`
|
||||
Authorize string `form:"authorize"`
|
||||
CodeChallenge string `form:"code_challenge,omitempty"`
|
||||
State string `form:"state"`
|
||||
Provider string `form:"provider"`
|
||||
Scope domain.Scopes `form:"scope[],omitempty"`
|
||||
ClientID domain.ClientID `form:"client_id"`
|
||||
Me domain.Me `form:"me"`
|
||||
RedirectURI domain.URL `form:"redirect_uri"`
|
||||
CodeChallengeMethod domain.CodeChallengeMethod `form:"code_challenge_method,omitempty"`
|
||||
ResponseType domain.ResponseType `form:"response_type"`
|
||||
Authorize string `form:"authorize"`
|
||||
CodeChallenge string `form:"code_challenge,omitempty"`
|
||||
State string `form:"state"`
|
||||
Provider string `form:"provider"`
|
||||
Scope domain.Scopes `form:"scope[],omitempty"`
|
||||
}
|
||||
|
||||
AuthExchangeRequest struct {
|
||||
|
@ -79,15 +79,15 @@ type (
|
|||
}
|
||||
|
||||
AuthExchangeResponse struct {
|
||||
Me domain.Me `json:"me"`
|
||||
Profile *AuthProfileResponse `json:"profile,omitempty"`
|
||||
Me string `json:"me"`
|
||||
}
|
||||
|
||||
AuthProfileResponse struct {
|
||||
Email *domain.Email `json:"email,omitempty"`
|
||||
Photo *domain.URL `json:"photo,omitempty"`
|
||||
URL *domain.URL `json:"url,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Photo string `json:"photo,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -95,7 +95,7 @@ func NewAuthAuthorizationRequest() *AuthAuthorizationRequest {
|
|||
return &AuthAuthorizationRequest{
|
||||
ClientID: domain.ClientID{},
|
||||
CodeChallenge: "",
|
||||
CodeChallengeMethod: &domain.CodeChallengeMethodUnd,
|
||||
CodeChallengeMethod: domain.CodeChallengeMethodUnd,
|
||||
Me: domain.Me{},
|
||||
RedirectURI: domain.URL{},
|
||||
ResponseType: domain.ResponseTypeUnd,
|
||||
|
@ -129,7 +129,7 @@ func NewAuthVerifyRequest() *AuthVerifyRequest {
|
|||
Authorize: "",
|
||||
ClientID: domain.ClientID{},
|
||||
CodeChallenge: "",
|
||||
CodeChallengeMethod: &domain.CodeChallengeMethodUnd,
|
||||
CodeChallengeMethod: domain.CodeChallengeMethodUnd,
|
||||
Me: domain.Me{},
|
||||
Provider: "",
|
||||
RedirectURI: domain.URL{},
|
||||
|
@ -139,7 +139,7 @@ func NewAuthVerifyRequest() *AuthVerifyRequest {
|
|||
}
|
||||
}
|
||||
|
||||
//nolint:funlen,cyclop
|
||||
//nolint:cyclop
|
||||
func (r *AuthVerifyRequest) bind(req *http.Request) error {
|
||||
indieAuthError := new(domain.Error)
|
||||
|
||||
|
@ -192,3 +192,27 @@ func (r *AuthExchangeRequest) bind(req *http.Request) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewAuthProfileResponse(in *domain.Profile) *AuthProfileResponse {
|
||||
out := new(AuthProfileResponse)
|
||||
|
||||
if in == nil {
|
||||
return out
|
||||
}
|
||||
|
||||
out.Name = in.Name
|
||||
|
||||
if in.URL != nil {
|
||||
out.URL = in.URL.String()
|
||||
}
|
||||
|
||||
if in.Email != nil {
|
||||
out.Email = in.Email.String()
|
||||
}
|
||||
|
||||
if in.Photo != nil {
|
||||
out.Photo = in.Photo.String()
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
|
|
@ -31,13 +31,14 @@ type Dependencies struct {
|
|||
authService auth.UseCase
|
||||
clients client.Repository
|
||||
clientService client.UseCase
|
||||
config *domain.Config
|
||||
matcher language.Matcher
|
||||
profiles profile.Repository
|
||||
sessions session.Repository
|
||||
users user.Repository
|
||||
config *domain.Config
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func TestAuthorize(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -85,7 +86,7 @@ func TestAuthorize(t *testing.T) {
|
|||
Clients: deps.clientService,
|
||||
Config: *deps.config,
|
||||
Matcher: deps.matcher,
|
||||
}).Handler().ServeHTTP(w, req)
|
||||
}).ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
|
||||
|
@ -98,7 +99,7 @@ func TestAuthorize(t *testing.T) {
|
|||
t.Errorf("%s %s = %d, want %d", req.Method, u.String(), resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
const expResult = `Authorize application`
|
||||
expResult := `Authorize ` + client.Name
|
||||
if result := string(body); !strings.Contains(result, expResult) {
|
||||
t.Errorf("%s %s = %s, want %s", req.Method, u.String(), result, expResult)
|
||||
}
|
||||
|
@ -113,7 +114,7 @@ func NewDependencies(tb testing.TB) Dependencies {
|
|||
users := userrepo.NewMemoryUserRepository()
|
||||
sessions := sessionrepo.NewMemorySessionRepository(*config)
|
||||
profiles := profilerepo.NewMemoryProfileRepository()
|
||||
authService := ucase.NewAuthUseCase(sessions, profiles, config)
|
||||
authService := ucase.NewAuthUseCase(sessions, profiles, *config)
|
||||
clientService := clientucase.NewClientUseCase(clients)
|
||||
|
||||
return Dependencies{
|
||||
|
|
|
@ -13,8 +13,8 @@ type (
|
|||
Me domain.Me
|
||||
RedirectURI *url.URL
|
||||
CodeChallengeMethod domain.CodeChallengeMethod
|
||||
Scope domain.Scopes
|
||||
CodeChallenge string
|
||||
Scope domain.Scopes
|
||||
}
|
||||
|
||||
ExchangeOptions struct {
|
||||
|
|
|
@ -12,13 +12,13 @@ import (
|
|||
)
|
||||
|
||||
type authUseCase struct {
|
||||
config *domain.Config
|
||||
sessions session.Repository
|
||||
profiles profile.Repository
|
||||
config domain.Config
|
||||
}
|
||||
|
||||
// NewAuthUseCase creates a new authentication use case.
|
||||
func NewAuthUseCase(sessions session.Repository, profiles profile.Repository, config *domain.Config) auth.UseCase {
|
||||
func NewAuthUseCase(sessions session.Repository, profiles profile.Repository, config domain.Config) auth.UseCase {
|
||||
return &authUseCase{
|
||||
config: config,
|
||||
sessions: sessions,
|
||||
|
|
|
@ -39,26 +39,24 @@ func NewHandler(opts NewHandlerOptions) *Handler {
|
|||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "" && r.Method != http.MethodGet {
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "" && r.Method != http.MethodGet {
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var head string
|
||||
head, r.URL.Path = urlutil.ShiftPath(r.URL.Path)
|
||||
var head string
|
||||
head, r.URL.Path = urlutil.ShiftPath(r.URL.Path)
|
||||
|
||||
switch head {
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
case "":
|
||||
h.handleRender(w, r)
|
||||
case "callback":
|
||||
h.handleCallback(w, r)
|
||||
}
|
||||
})
|
||||
switch head {
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
case "":
|
||||
h.handleRender(w, r)
|
||||
case "callback":
|
||||
h.handleCallback(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleRender(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -93,7 +91,7 @@ func (h *Handler) handleRender(w http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
}
|
||||
|
||||
//nolint:unlen
|
||||
//nolint:funlen
|
||||
func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "" && r.Method != http.MethodGet {
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
|
|
|
@ -10,10 +10,10 @@ import (
|
|||
|
||||
type ClientCallbackRequest struct {
|
||||
Error domain.ErrorCode `form:"error,omitempty"`
|
||||
Iss domain.ClientID `form:"iss"`
|
||||
Code string `form:"code"`
|
||||
Iss domain.ClientID `form:"iss,omitempty"`
|
||||
Code string `form:"code,omitempty"`
|
||||
ErrorDescription string `form:"error_description,omitempty"`
|
||||
State string `form:"state"`
|
||||
State string `form:"state,omitempty"`
|
||||
}
|
||||
|
||||
func (req *ClientCallbackRequest) bind(r *http.Request) error {
|
||||
|
|
|
@ -41,7 +41,7 @@ func TestRead(t *testing.T) {
|
|||
Config: *deps.config,
|
||||
Matcher: deps.matcher,
|
||||
Tokens: deps.tokenService,
|
||||
}).Handler().ServeHTTP(w, req)
|
||||
}).ServeHTTP(w, req)
|
||||
|
||||
if resp := w.Result(); resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("%s %s = %d, want %d", req.Method, req.RequestURI, resp.StatusCode, http.StatusOK)
|
||||
|
@ -58,7 +58,7 @@ func NewDependencies(tb testing.TB) Dependencies {
|
|||
tokens := tokenrepo.NewMemoryTokenRepository()
|
||||
profiles := profilerepo.NewMemoryProfileRepository()
|
||||
tokenService := tokenucase.NewTokenUseCase(tokenucase.Config{
|
||||
Config: config,
|
||||
Config: *config,
|
||||
Profiles: profiles,
|
||||
Sessions: sessions,
|
||||
Tokens: tokens,
|
||||
|
|
|
@ -8,27 +8,40 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/tomnomnom/linkheader"
|
||||
"golang.org/x/exp/slices"
|
||||
"willnorris.com/go/microformats"
|
||||
|
||||
"source.toby3d.me/toby3d/auth/internal/client"
|
||||
"source.toby3d.me/toby3d/auth/internal/common"
|
||||
"source.toby3d.me/toby3d/auth/internal/domain"
|
||||
"source.toby3d.me/toby3d/auth/internal/httputil"
|
||||
)
|
||||
|
||||
type httpClientRepository struct {
|
||||
client *http.Client
|
||||
}
|
||||
type (
|
||||
//nolint:tagliatelle,lll
|
||||
Response struct {
|
||||
TicketEndpoint domain.URL `json:"ticket_endpoint"`
|
||||
AuthorizationEndpoint domain.URL `json:"authorization_endpoint"`
|
||||
IntrospectionEndpoint domain.URL `json:"introspection_endpoint"`
|
||||
RevocationEndpoint domain.URL `json:"revocation_endpoint,omitempty"`
|
||||
ServiceDocumentation domain.URL `json:"service_documentation,omitempty"`
|
||||
TokenEndpoint domain.URL `json:"token_endpoint"`
|
||||
UserinfoEndpoint domain.URL `json:"userinfo_endpoint,omitempty"`
|
||||
Microsub domain.URL `json:"microsub"`
|
||||
Issuer domain.URL `json:"issuer"`
|
||||
Micropub domain.URL `json:"micropub"`
|
||||
GrantTypesSupported []domain.GrantType `json:"grant_types_supported,omitempty"`
|
||||
IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported,omitempty"`
|
||||
RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported,omitempty"`
|
||||
ScopesSupported []domain.Scope `json:"scopes_supported,omitempty"`
|
||||
ResponseTypesSupported []domain.ResponseType `json:"response_types_supported,omitempty"`
|
||||
CodeChallengeMethodsSupported []domain.CodeChallengeMethod `json:"code_challenge_methods_supported"`
|
||||
AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultMaxRedirectsCount int = 10
|
||||
|
||||
hApp string = "h-app"
|
||||
hXApp string = "h-x-app"
|
||||
propertyLogo string = "logo"
|
||||
propertyName string = "name"
|
||||
propertyURL string = "url"
|
||||
relRedirectURI string = "redirect_uri"
|
||||
httpClientRepository struct {
|
||||
client *http.Client
|
||||
}
|
||||
)
|
||||
|
||||
func NewHTTPClientRepository(c *http.Client) client.Repository {
|
||||
|
@ -43,6 +56,18 @@ func (httpClientRepository) Create(_ context.Context, _ domain.Client) error {
|
|||
}
|
||||
|
||||
func (repo httpClientRepository) Get(ctx context.Context, cid domain.ClientID) (*domain.Client, error) {
|
||||
out := &domain.Client{
|
||||
ID: cid,
|
||||
RedirectURI: make([]*url.URL, 0),
|
||||
Logo: nil,
|
||||
URL: nil,
|
||||
Name: "",
|
||||
}
|
||||
|
||||
if cid.IsLocalhost() {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
resp, err := repo.client.Get(cid.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make a request to the client: %w", err)
|
||||
|
@ -52,77 +77,82 @@ func (repo httpClientRepository) Get(ctx context.Context, cid domain.ClientID) (
|
|||
return nil, fmt.Errorf("%w: status on client page is not 200", client.ErrNotExist)
|
||||
}
|
||||
|
||||
client := &domain.Client{
|
||||
ID: cid,
|
||||
RedirectURI: make([]*url.URL, 0),
|
||||
Logo: make([]*url.URL, 0),
|
||||
URL: make([]*url.URL, 0),
|
||||
Name: make([]string, 0),
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read response body: %w", err)
|
||||
}
|
||||
|
||||
extract(resp.Body, resp.Request.URL, client, resp.Header.Get(common.HeaderLink))
|
||||
// NOTE(toby3d): fetch redirect uri's and application profile from HTML nodes
|
||||
mf2 := microformats.Parse(bytes.NewReader(body), resp.Request.URL)
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
//nolint:gocognit,cyclop
|
||||
func extract(r io.Reader, u *url.URL, dst *domain.Client, header string) {
|
||||
body, _ := io.ReadAll(r)
|
||||
|
||||
for _, endpoint := range httputil.ExtractEndpoints(bytes.NewReader(body), u, header, relRedirectURI) {
|
||||
if !containsUrl(dst.RedirectURI, endpoint) {
|
||||
dst.RedirectURI = append(dst.RedirectURI, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
for _, itemType := range []string{hApp, hXApp} {
|
||||
for _, name := range httputil.ExtractProperty(bytes.NewReader(body), u, itemType, propertyName) {
|
||||
if n, ok := name.(string); ok && !slices.Contains(dst.Name, n) {
|
||||
dst.Name = append(dst.Name, n)
|
||||
}
|
||||
}
|
||||
|
||||
for _, logo := range httputil.ExtractProperty(bytes.NewReader(body), u, itemType, propertyLogo) {
|
||||
var logoURL *url.URL
|
||||
var err error
|
||||
|
||||
switch l := logo.(type) {
|
||||
case string:
|
||||
logoURL, err = url.Parse(l)
|
||||
case map[string]string:
|
||||
if value, ok := l["value"]; ok {
|
||||
logoURL, err = url.Parse(value)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil || containsUrl(dst.Logo, logoURL) {
|
||||
continue
|
||||
}
|
||||
|
||||
dst.Logo = append(dst.Logo, logoURL)
|
||||
}
|
||||
|
||||
for _, property := range httputil.ExtractProperty(bytes.NewReader(body), u, itemType, propertyURL) {
|
||||
prop, ok := property.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if u, err := url.Parse(prop); err == nil && !containsUrl(dst.URL, u) {
|
||||
dst.URL = append(dst.URL, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func containsUrl(src []*url.URL, find *url.URL) bool {
|
||||
for i := range src {
|
||||
if src[i].String() != find.String() {
|
||||
for i := range mf2.Items {
|
||||
if !slices.Contains(mf2.Items[i].Type, common.HApp) &&
|
||||
!slices.Contains(mf2.Items[i].Type, common.HXApp) {
|
||||
continue
|
||||
}
|
||||
|
||||
return true
|
||||
parseProfile(mf2.Items[i].Properties, out)
|
||||
}
|
||||
|
||||
return false
|
||||
for _, val := range mf2.Rels[common.RelRedirectURI] {
|
||||
var u *url.URL
|
||||
if u, err = url.Parse(val); err == nil {
|
||||
out.RedirectURI = append(out.RedirectURI, u)
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE(toby3d): fetch redirect uri's from Link header
|
||||
for _, link := range linkheader.Parse(resp.Header.Get(common.HeaderLink)) {
|
||||
if link.Rel != common.RelRedirectURI {
|
||||
continue
|
||||
}
|
||||
|
||||
var u *url.URL
|
||||
if u, err = url.Parse(link.URL); err == nil {
|
||||
out.RedirectURI = append(out.RedirectURI, u)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parseProfile(src map[string][]any, dst *domain.Client) {
|
||||
for _, val := range src[common.PropertyName] {
|
||||
v, ok := val.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
dst.Name = v
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
for _, val := range src[common.PropertyURL] {
|
||||
v, ok := val.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
if dst.URL, err = url.Parse(v); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
for _, val := range src[common.PropertyLogo] {
|
||||
v, ok := val.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
if dst.Logo, err = url.Parse(v); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,9 +73,9 @@ func TestGet(t *testing.T) {
|
|||
func testHandler(tb testing.TB, client domain.Client) http.Handler {
|
||||
tb.Helper()
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
|
||||
w.Header().Set(common.HeaderLink, `<`+client.RedirectURI[0].String()+`>; rel="redirect_uri"`)
|
||||
fmt.Fprintf(w, testBody, client.Name[0], client.URL[0], client.Logo[0], client.RedirectURI[1])
|
||||
w.Header().Set(common.HeaderLink, `<`+client.RedirectURI[1].String()+`>; rel="redirect_uri"`)
|
||||
fmt.Fprintf(w, testBody, client.Name, client.URL, client.Logo, client.RedirectURI[0])
|
||||
})
|
||||
}
|
||||
|
|
|
@ -22,10 +22,10 @@ func TestDiscovery(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
expError error
|
||||
in *domain.Client
|
||||
out *domain.Client
|
||||
expError error
|
||||
name string
|
||||
}{{
|
||||
name: "default",
|
||||
in: testClient,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package common
|
||||
|
||||
const charsetUTF8 = "charset=UTF-8"
|
||||
|
||||
const (
|
||||
MIMEApplicationForm string = "application/x-www-form-urlencoded"
|
||||
MIMEApplicationJSON string = "application/json"
|
||||
|
@ -8,8 +10,6 @@ const (
|
|||
MIMETextHTMLCharsetUTF8 string = MIMETextHTML + "; " + charsetUTF8
|
||||
MIMETextPlain string = "text/plain"
|
||||
MIMETextPlainCharsetUTF8 string = MIMETextPlain + "; " + charsetUTF8
|
||||
|
||||
charsetUTF8 = "charset=UTF-8"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -27,4 +27,29 @@ const (
|
|||
HeaderXCSRFToken string = "X-CSRF-Token"
|
||||
)
|
||||
|
||||
const (
|
||||
HApp string = "h-app"
|
||||
HCard string = "h-card"
|
||||
HXApp string = "h-x-app"
|
||||
)
|
||||
|
||||
const (
|
||||
PropertyEmail string = "email"
|
||||
PropertyLogo string = "logo"
|
||||
PropertyName string = "name"
|
||||
PropertyPhoto string = "photo"
|
||||
PropertyURL string = "url"
|
||||
)
|
||||
|
||||
const (
|
||||
RelAuthn string = "authn"
|
||||
RelAuthorizationEndpoint string = "authorization_endpoint"
|
||||
RelIndieAuthMetadata string = "indieauth-metadata"
|
||||
RelMicropub string = "micropub"
|
||||
RelMicrosub string = "microsub"
|
||||
RelRedirectURI string = "redirect_uri"
|
||||
RelTicketEndpoint string = "ticket_endpoint"
|
||||
RelTokenEndpoint string = "token_endpoint"
|
||||
)
|
||||
|
||||
const Und string = "und"
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
package domain
|
||||
|
||||
import "net/url"
|
||||
|
||||
type App struct {
|
||||
Logo []*url.URL
|
||||
URL []*url.URL
|
||||
Name []string
|
||||
}
|
||||
|
||||
// GetName safe returns first name, if any.
|
||||
func (a App) GetName() string {
|
||||
if len(a.Name) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return a.Name[0]
|
||||
}
|
||||
|
||||
// GetURL safe returns first URL, if any.
|
||||
func (a App) GetURL() *url.URL {
|
||||
if len(a.URL) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return a.URL[0]
|
||||
}
|
||||
|
||||
// GetLogo safe returns first logo, if any.
|
||||
func (a App) GetLogo() *url.URL {
|
||||
if len(a.Logo) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return a.Logo[0]
|
||||