mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-10-31 22:40:01 +00:00
Merge branch 'main' into webfinger_rework
This commit is contained in:
commit
54d9778c4c
215 changed files with 11375 additions and 3732 deletions
|
@ -1,8 +1,9 @@
|
|||
.github
|
||||
cmd
|
||||
.vscode
|
||||
archive
|
||||
dist
|
||||
docs
|
||||
example
|
||||
internal
|
||||
scripts
|
||||
test
|
||||
testrig
|
||||
|
|
|
@ -38,6 +38,7 @@ steps:
|
|||
- apk update --no-cache && apk add git
|
||||
- CGO_ENABLED=0 GTS_DB_TYPE="sqlite" GTS_DB_ADDRESS=":memory:" go test ./...
|
||||
- CGO_ENABLED=0 ./test/cliparsing.sh
|
||||
- CGO_ENABLED=0 ./test/envparsing.sh
|
||||
when:
|
||||
event:
|
||||
include:
|
||||
|
@ -145,6 +146,6 @@ steps:
|
|||
|
||||
---
|
||||
kind: signature
|
||||
hmac: f3cf4e422d9ce7dc0a881da429db628232e2f9e91383ee5a33cf4f13542c0a23
|
||||
hmac: adfcc11559717e4e371e714f3ac19ab528208f678961436f316f491bf82de8ad
|
||||
|
||||
...
|
||||
|
|
66
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
66
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
title: "[bug] Issue Title"
|
||||
labels: [bug]
|
||||
assignees:
|
||||
-
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Please be cautious with the sensitive information/logs while filing the issue.
|
||||
- type: textarea
|
||||
id: desc
|
||||
attributes:
|
||||
label: Describe the bug with a clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: GoToSocial-Version
|
||||
attributes:
|
||||
label: What's your GoToSocial Version?
|
||||
description: Enter the Version of your GoToSocial Installation
|
||||
placeholder: e.g. v0.3.4
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: GoToSocial-Arch
|
||||
attributes:
|
||||
label: GoToSocial Arch
|
||||
description: What architecture do you use and did you do a Binary or Docker installation?
|
||||
placeholder: e.g. arm64 Docker or arm64 Binary
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Enter exactly what happened.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: what-expected
|
||||
attributes:
|
||||
label: What you expected to happen?
|
||||
description: Enter what you expected to happen.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: how-to-reproduce
|
||||
attributes:
|
||||
label: How to reproduce it?
|
||||
description: As minimally and precisely as possible.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: anything-else
|
||||
attributes:
|
||||
label: Anything else we need to know?
|
||||
validations:
|
||||
required: false
|
8
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
8
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: Custom issue template
|
||||
about: Describe this issue template's purpose here.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
44
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
title: "[feature] Issue Title"
|
||||
labels: [enhancement]
|
||||
assignees:
|
||||
-
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature request!
|
||||
|
||||
- type: textarea
|
||||
id: desc
|
||||
attributes:
|
||||
label: Is your feature request related to a problem ?
|
||||
description: Give a clear and concise description of what the problem is.
|
||||
placeholder: ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: prop-solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like.
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered.
|
||||
description: A clear and concise description of any alternative solutions or features you've considered. If nothing, please enter `NONE`
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-ctxt
|
||||
attributes:
|
||||
label: Additional context.
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -20,4 +20,10 @@ dist/
|
|||
web/assets/swagger.yaml
|
||||
|
||||
# exludes docker-volume from exemple/docker-compose
|
||||
example/docker-compose/docker-volume
|
||||
example/docker-compose/docker-volume
|
||||
|
||||
# excludes debug build
|
||||
cmd/gotosocial/__debug_bin
|
||||
|
||||
# ignore f0x' nix-shell
|
||||
shell.nix
|
|
@ -3,16 +3,12 @@ project_name: gotosocial
|
|||
before:
|
||||
# https://goreleaser.com/customization/hooks/
|
||||
hooks:
|
||||
# tidy up and lint
|
||||
- go mod tidy
|
||||
- go fmt ./...
|
||||
# generate the swagger.yaml file using go-swagger and bundle it into the assets directory
|
||||
- swagger generate spec -o docs/api/swagger.yaml --scan-models
|
||||
- sed -i "s/REPLACE_ME/{{ incpatch .Version }}/" docs/api/swagger.yaml
|
||||
- cp docs/api/swagger.yaml web/assets/swagger.yaml
|
||||
# install and bundle the web assets and styling
|
||||
- yarn install --cwd web/gotosocial-styling
|
||||
- node web/gotosocial-styling/index.js --build-dir="web/assets"
|
||||
- swagger generate spec -o web/assets/swagger.yaml --scan-models
|
||||
- sed -i "s/REPLACE_ME/{{ incpatch .Version }}/" web/assets/swagger.yaml
|
||||
# bundle web assets
|
||||
- yarn install --cwd web/source
|
||||
- scripts/bundle.sh
|
||||
builds:
|
||||
# https://goreleaser.com/customization/build/
|
||||
-
|
||||
|
@ -67,6 +63,10 @@ dockers:
|
|||
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||
extra_files:
|
||||
- web
|
||||
- go.mod
|
||||
- go.sum
|
||||
- cmd
|
||||
- internal
|
||||
-
|
||||
use: buildx
|
||||
goos: linux
|
||||
|
@ -82,6 +82,10 @@ dockers:
|
|||
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||
extra_files:
|
||||
- web
|
||||
- go.mod
|
||||
- go.sum
|
||||
- cmd
|
||||
- internal
|
||||
-
|
||||
use: buildx
|
||||
goos: linux
|
||||
|
@ -98,6 +102,10 @@ dockers:
|
|||
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||
extra_files:
|
||||
- web
|
||||
- go.mod
|
||||
- go.sum
|
||||
- cmd
|
||||
- internal
|
||||
-
|
||||
use: buildx
|
||||
goos: linux
|
||||
|
@ -114,6 +122,10 @@ dockers:
|
|||
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||
extra_files:
|
||||
- web
|
||||
- go.mod
|
||||
- go.sum
|
||||
- cmd
|
||||
- internal
|
||||
docker_manifests:
|
||||
- name_template: superseriousbusiness/{{ .ProjectName }}:{{ .Version }}
|
||||
image_templates:
|
||||
|
@ -136,7 +148,8 @@ archives:
|
|||
- README.md
|
||||
- CHANGELOG*
|
||||
# web assets
|
||||
- web
|
||||
- web/assets
|
||||
- web/template
|
||||
# example config files
|
||||
- example/config.yaml
|
||||
- example/gotosocial.service
|
||||
|
|
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Package",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/cmd/gotosocial",
|
||||
"args": [
|
||||
"testrig", "start"
|
||||
],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"go.lintTool":"golangci-lint",
|
||||
"go.lintFlags": [
|
||||
"--fast"
|
||||
]
|
||||
}
|
|
@ -28,7 +28,8 @@ Check the [issues](https://github.com/superseriousbusiness/gotosocial/issues) to
|
|||
- [Updating Swagger docs](#updating-swagger-docs)
|
||||
- [CI/CD configuration](#cicd-configuration)
|
||||
- [Release Checklist](#release-checklist)
|
||||
- [Building releases and Docker containers](#building-releases-and-docker-containers)
|
||||
- [What if something goes wrong?](#what-if-something-goes-wrong)
|
||||
- [Building Docker containers](#building-docker-containers)
|
||||
- [With GoReleaser](#with-goreleaser)
|
||||
- [Manually](#manually)
|
||||
- [Financial Compensation](#financial-compensation)
|
||||
|
@ -70,19 +71,21 @@ To work with the stylesheet for templates, you need [Node.js](https://nodejs.org
|
|||
To install Yarn dependencies:
|
||||
|
||||
```bash
|
||||
yarn install --cwd web/gotosocial-styling
|
||||
yarn install --cwd web/source
|
||||
```
|
||||
|
||||
To recompile bundles:
|
||||
|
||||
```bash
|
||||
node web/gotosocial-styling/index.js --build-dir="web/assets"
|
||||
BUDO_BUILD=1 node web/source
|
||||
```
|
||||
|
||||
You can do automatic live-reloads of bundles with:
|
||||
Or you can run livereloading in development. It will start a webserver (ip/port printed in console, default localhost:8081), while also keeping the bundles
|
||||
up-to-date on disk. You can access the user/admin panels at localhost:8080/user, localhost:8080/admin, or run in tandem with the GoToSocial testrig, which will also
|
||||
serve the updated bundles from disk.
|
||||
|
||||
``` bash
|
||||
NODE_ENV=development node web/gotosocial-styling/index.js --build-dir="web/assets"
|
||||
NODE_ENV=development node web/source
|
||||
```
|
||||
|
||||
### Golang forking quirks
|
||||
|
@ -185,7 +188,7 @@ Finally, to run tests against both database types one after the other, use:
|
|||
|
||||
### CLI Tests
|
||||
|
||||
In [./test/cliparsing.sh](./test/cliparsing.sh) there are a bunch of tests for making sure that CLI flags, config, and environment variables get parsed as expected.
|
||||
In [./test/cliparsing.sh](./test/cliparsing.sh) and [./test/envparsing.sh](./test/envparsing.sh) there are a bunch of tests for making sure that CLI flags, config, and environment variables get parsed as expected.
|
||||
|
||||
Although these tests *are* part of the CI/CD testing process, you probably won't need to worry too much about running them yourself. That is, unless you're messing about with code inside the `main` package in `cmd/gotosocial`, or inside the `config` package in `internal/config`.
|
||||
|
||||
|
@ -299,7 +302,9 @@ That is: Delete the tag.
|
|||
|
||||
Either way, once we've fixed the issue, we just start from the top of this list again. Version numbers are cheap. It's cheap to burn them.
|
||||
|
||||
## Building releases and Docker containers
|
||||
## Building Docker containers
|
||||
|
||||
For both of the below methods, you need to have [Docker buildx](https://docs.docker.com/buildx/working-with-buildx/) installed.
|
||||
|
||||
### With GoReleaser
|
||||
|
||||
|
@ -311,22 +316,30 @@ Normally, these processes are handled by Drone (see CI/CD above). However, you c
|
|||
|
||||
To do this, first [install GoReleaser](https://goreleaser.com/install/).
|
||||
|
||||
Then, to create snapshot builds, do:
|
||||
Then install GoSwagger as described in [the Swagger section](#updating-swagger-docs).
|
||||
|
||||
Then install Node and Yarn as described in [Stylesheet / Web dev](#stylesheet--web-dev).
|
||||
|
||||
Finally, to create a snapshot build, do:
|
||||
|
||||
```bash
|
||||
goreleaser release --rm-dist --snapshot
|
||||
goreleaser --rm-dist --snapshot
|
||||
```
|
||||
|
||||
If all goes according to plan, you should now have a bunch of multiple-architecture binaries and tars inside the `./dist` folder, and a snapshot Docker image should be built (check your terminal output for version).
|
||||
If all goes according to plan, you should now have a bunch of multiple-architecture binaries and tars inside the `./dist` folder, and snapshot Docker images should be built (check your terminal output for version).
|
||||
|
||||
### Manually
|
||||
|
||||
If you prefer a simple approach with fewer dependencies, you can also just build a Docker container manually in the following way:
|
||||
If you prefer a simple approach to building a Docker container, with fewer dependencies, you can also just build in the following way:
|
||||
|
||||
```bash
|
||||
./scripts/build.sh && docker build -t superseriousbusiness/gotosocial:latest .
|
||||
./scripts/build.sh && docker buildx build -t superseriousbusiness/gotosocial:latest .
|
||||
```
|
||||
|
||||
The above command first builds the `gotosocial` binary, then invokes Docker buildx to build the container image.
|
||||
|
||||
You don't need to install go-swagger, Node, or Yarn to build Docker images this way.
|
||||
|
||||
## Financial Compensation
|
||||
|
||||
Right now there's no structure in place for financial compensation for pull requests and code. This is simply because there's no money being made on the project apart from the very small weekly Liberapay donations.
|
||||
|
|
35
Dockerfile
35
Dockerfile
|
@ -1,26 +1,31 @@
|
|||
# syntax=docker/dockerfile:1.3
|
||||
# stage 1: generate up-to-date swagger.yaml to put in the final container
|
||||
FROM --platform=${BUILDPLATFORM} quay.io/goswagger/swagger:v0.29.0 AS swagger
|
||||
|
||||
# bundle the admin webapp
|
||||
FROM --platform=${BUILDPLATFORM} node:17.6.0-alpine3.15 AS admin_builder
|
||||
RUN apk update && apk upgrade --no-cache
|
||||
RUN apk add git
|
||||
COPY go.mod /go/src/github.com/superseriousbusiness/gotosocial/go.mod
|
||||
COPY go.sum /go/src/github.com/superseriousbusiness/gotosocial/go.sum
|
||||
COPY cmd /go/src/github.com/superseriousbusiness/gotosocial/cmd
|
||||
COPY internal /go/src/github.com/superseriousbusiness/gotosocial/internal
|
||||
WORKDIR /go/src/github.com/superseriousbusiness/gotosocial
|
||||
RUN swagger generate spec -o /go/src/github.com/superseriousbusiness/gotosocial/swagger.yaml --scan-models
|
||||
|
||||
RUN git clone https://github.com/superseriousbusiness/gotosocial-admin
|
||||
WORKDIR /gotosocial-admin
|
||||
# stage 2: generate the web/assets/dist bundles
|
||||
FROM --platform=${BUILDPLATFORM} node:16.15.1-alpine3.15 AS bundler
|
||||
|
||||
RUN npm install
|
||||
RUN node index.js
|
||||
COPY web web
|
||||
RUN yarn install --cwd web/source && \
|
||||
BUDO_BUILD=1 node web/source && \
|
||||
rm -r web/source
|
||||
|
||||
FROM --platform=${TARGETPLATFORM} alpine:3.15.0 AS executor
|
||||
# stage 3: build the executor container
|
||||
FROM --platform=${TARGETPLATFORM} alpine:3.15.4 as executor
|
||||
|
||||
# copy over the binary from the first stage
|
||||
# copy the dist binary created by goreleaser or build.sh
|
||||
COPY --chown=1000:1000 gotosocial /gotosocial/gotosocial
|
||||
|
||||
# copy over the web directory with templates etc
|
||||
COPY --chown=1000:1000 web /gotosocial/web
|
||||
|
||||
# copy over the admin directory
|
||||
COPY --chown=1000:1000 --from=admin_builder /gotosocial-admin/public /gotosocial/web/assets/admin
|
||||
# copy over the web directories with templates, assets etc
|
||||
COPY --chown=1000:1000 --from=bundler web /gotosocial/web
|
||||
COPY --chown=1000:1000 --from=swagger /go/src/github.com/superseriousbusiness/gotosocial/swagger.yaml web/assets/swagger.yaml
|
||||
|
||||
WORKDIR "/gotosocial"
|
||||
ENTRYPOINT [ "/gotosocial/gotosocial", "server", "start" ]
|
||||
|
|
|
@ -165,7 +165,7 @@
|
|||
}
|
||||
|
||||
// build client api modules
|
||||
authModule := auth.New(dbService, oauthServer, idp)
|
||||
authModule := auth.New(dbService, idp, processor)
|
||||
accountModule := account.New(processor)
|
||||
instanceModule := instance.New(processor)
|
||||
appsModule := app.New(processor)
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
}
|
||||
|
||||
// build client api modules
|
||||
authModule := auth.New(dbService, oauthServer, idp)
|
||||
authModule := auth.New(dbService, idp, processor)
|
||||
accountModule := account.New(processor)
|
||||
instanceModule := instance.New(processor)
|
||||
appsModule := app.New(processor)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Admin Control Panel
|
||||
|
||||
[gotosocial-admin](https://github.com/superseriousbusiness/gotosocial-admin) is a simple webclient that uses the [admin api routes](https://docs.gotosocial.org/en/latest/api/swagger/#operations-tag-admin) to manage your instance. It uses the same OAUTH mechanism as normal clients (with scope: admin), and as such can be hosted anywhere, separately from your instance, or run locally. A public installation is available here: [https://gts.superseriousbusiness.org/admin](https://gts.superseriousbusiness.org/admin).
|
||||
The GoToSocial admin panel is a simple webclient that uses the [admin api routes](https://docs.gotosocial.org/en/latest/api/swagger/#operations-tag-admin) to manage your instance. It uses the same OAUTH mechanism as normal clients (with scope: admin), and as such can be hosted anywhere, separately from your instance, or run locally. A public installation is available here: [https://gts.superseriousbusiness.org/admin](https://gts.superseriousbusiness.org/admin).
|
||||
|
||||
## Using the panel
|
||||
To use the Admin API your account has to be promoted as such:
|
||||
|
@ -16,11 +16,11 @@ any other client.
|
|||
|
||||
You can change the instance's settings like the title and descriptions, and add/remove/change domain blocks including a bulk import/export.
|
||||
|
||||
## Installing the panel
|
||||
## Building the panel
|
||||
Build requirements: some version of [Node.js](https://nodejs.org) and yarn.
|
||||
```
|
||||
git clone https://github.com/superseriousbusiness/gotosocial-admin.git && cd gotosocial-admin
|
||||
yarn install
|
||||
node index.js
|
||||
yarn install --cwd web/source
|
||||
BUDO_BUILD=1 node web/source
|
||||
```
|
||||
This will compile a static bundle in `public/`, which can be copied to any webhost, or put into your GoToSocial installation in the `web/admin` directory.
|
||||
|
||||
See also: [Contributing.md Stylesheet / Web dev](https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#stylesheet--web-dev)
|
|
@ -692,7 +692,7 @@ definitions:
|
|||
text_url:
|
||||
description: |-
|
||||
A shorter URL for the attachment.
|
||||
Not currently used.
|
||||
In our case, we just give the URL again since we don't create smaller URLs.
|
||||
type: string
|
||||
x-go-name: TextURL
|
||||
type:
|
||||
|
@ -1894,8 +1894,10 @@ paths:
|
|||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal error
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Application:
|
||||
- write:accounts
|
||||
|
@ -1924,6 +1926,10 @@ paths:
|
|||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:accounts
|
||||
|
@ -1952,6 +1958,10 @@ paths:
|
|||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:blocks
|
||||
|
@ -1999,6 +2009,10 @@ paths:
|
|||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:follows
|
||||
|
@ -2029,6 +2043,10 @@ paths:
|
|||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:accounts
|
||||
|
@ -2059,6 +2077,10 @@ paths:
|
|||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:accounts
|
||||
|
@ -2134,6 +2156,10 @@ paths:
|
|||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:accounts
|
||||
|
@ -2162,6 +2188,10 @@ paths:
|
|||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:blocks
|
||||
|
@ -2190,6 +2220,10 @@ paths:
|
|||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:follows
|
||||
|
@ -2215,6 +2249,12 @@ paths:
|
|||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:accounts
|
||||
|
@ -2247,6 +2287,10 @@ paths:
|
|||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:accounts
|
||||
|
@ -2313,6 +2357,12 @@ paths:
|
|||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:accounts
|
||||
|
@ -2335,6 +2385,10 @@ paths:
|
|||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:accounts
|
||||
|
@ -2372,6 +2426,12 @@ paths:
|
|||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- admin
|
||||
|
@ -2406,10 +2466,18 @@ paths:
|
|||
$ref: '#/definitions/emoji'
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"409":
|
||||
description: conflict -- domain/shortcode combo for emoji already exists
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- admin
|
||||
|
@ -2439,10 +2507,16 @@ paths:
|
|||
type: array
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- admin
|
||||
|
@ -2511,8 +2585,16 @@ paths:
|
|||
$ref: '#/definitions/domainBlock'
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- admin
|
||||
|
@ -2537,10 +2619,16 @@ paths:
|
|||
$ref: '#/definitions/domainBlock'
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- admin
|
||||
|
@ -2564,10 +2652,16 @@ paths:
|
|||
$ref: '#/definitions/domainBlock'
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- admin
|
||||
|
@ -2599,8 +2693,16 @@ paths:
|
|||
asynchronously after the request completes.
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- admin
|
||||
|
@ -2660,10 +2762,14 @@ paths:
|
|||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"422":
|
||||
description: unprocessable
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal error
|
||||
description: internal server error
|
||||
summary: Register a new application on this instance.
|
||||
tags:
|
||||
- apps
|
||||
|
@ -2714,6 +2820,10 @@ paths:
|
|||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:blocks
|
||||
|
@ -2753,10 +2863,12 @@ paths:
|
|||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:follows
|
||||
|
@ -2785,10 +2897,10 @@ paths:
|
|||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
|
@ -2817,10 +2929,10 @@ paths:
|
|||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
|
@ -2843,6 +2955,8 @@ paths:
|
|||
description: Instance information.
|
||||
schema:
|
||||
$ref: '#/definitions/instance'
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal error
|
||||
summary: View instance information.
|
||||
|
@ -2909,6 +3023,14 @@ paths:
|
|||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- admin
|
||||
|
@ -2952,10 +3074,10 @@ paths:
|
|||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"422":
|
||||
description: unprocessable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:media
|
||||
|
@ -2982,10 +3104,12 @@ paths:
|
|||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"422":
|
||||
description: unprocessable
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:media
|
||||
|
@ -3036,10 +3160,12 @@ paths:
|
|||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"422":
|
||||
description: unprocessable
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:media
|
||||
|
@ -3141,6 +3267,12 @@ paths:
|
|||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:search
|
||||
|
@ -3226,10 +3358,14 @@ paths:
|
|||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal error
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:statuses
|
||||
|
@ -3263,6 +3399,10 @@ paths:
|
|||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:statuses
|
||||
|
@ -3288,10 +3428,14 @@ paths:
|
|||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal error
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:statuses
|
||||
|
@ -3324,6 +3468,10 @@ paths:
|
|||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:statuses
|
||||
|
@ -3354,6 +3502,10 @@ paths:
|
|||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:statuses
|
||||
|
@ -3386,6 +3538,10 @@ paths:
|
|||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:accounts
|
||||
|
@ -3419,6 +3575,10 @@ paths:
|
|||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:statuses
|
||||
|
@ -3481,6 +3641,10 @@ paths:
|
|||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:statuses
|
||||
|
@ -3511,6 +3675,10 @@ paths:
|
|||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:statuses
|
||||
|
@ -3778,6 +3946,8 @@ paths:
|
|||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal error
|
||||
security:
|
||||
|
|
38
docs/configuration/advanced.md
Normal file
38
docs/configuration/advanced.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Advanced
|
||||
|
||||
Advanced settings options are provided for the sake of allowing admins to tune their instance to their liking.
|
||||
|
||||
These are set to sensible defaults, so most server admins won't need to touch them or think about them.
|
||||
|
||||
**Changing these settings if you don't know what you're doing may break your instance**.
|
||||
|
||||
## Settings
|
||||
|
||||
```yaml
|
||||
#############################
|
||||
##### ADVANCED SETTINGS #####
|
||||
#############################
|
||||
|
||||
# Advanced settings pertaining to http timeouts, security, cookies, and more.
|
||||
#
|
||||
# ONLY ADJUST THESE SETTINGS IF YOU KNOW WHAT YOU ARE DOING!
|
||||
#
|
||||
# Most users will not need to (and should not) touch these settings, since
|
||||
# they are set to sensible defaults, and may break if they are changed.
|
||||
#
|
||||
# Nevertheless, they are provided for the sake of allowing server admins to
|
||||
# tweak their instance for performance or security reasons.
|
||||
|
||||
# String. Value of the SameSite attribute of cookies set by GoToSocial.
|
||||
# Defaults to 'lax' to ensure that the OIDC flow does not break, which is
|
||||
# fine in most cases. If you want to harden your instance against CSRF attacks
|
||||
# and don't mind if some login-related things might break, you can set this
|
||||
# to 'strict' instead.
|
||||
#
|
||||
# For an overview of what this does, see:
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
|
||||
#
|
||||
# Options: ["lax", "strict"]
|
||||
# Default: "lax"
|
||||
advanced-cookies-samesite: "lax"
|
||||
```
|
|
@ -428,3 +428,30 @@ syslog-protocol: "udp"
|
|||
# String. Address:port to send syslog logs to. Leave empty to connect to local syslog.
|
||||
# Default: "localhost:514"
|
||||
syslog-address: "localhost:514"
|
||||
|
||||
#############################
|
||||
##### ADVANCED SETTINGS #####
|
||||
#############################
|
||||
|
||||
# Advanced settings pertaining to http timeouts, security, cookies, and more.
|
||||
#
|
||||
# ONLY ADJUST THESE SETTINGS IF YOU KNOW WHAT YOU ARE DOING!
|
||||
#
|
||||
# Most users will not need to (and should not) touch these settings, since
|
||||
# they are set to sensible defaults, and may break if they are changed.
|
||||
#
|
||||
# Nevertheless, they are provided for the sake of allowing server admins to
|
||||
# tweak their instance for performance or security reasons.
|
||||
|
||||
# String. Value of the SameSite attribute of cookies set by GoToSocial.
|
||||
# Defaults to 'lax' to ensure that the OIDC flow does not break, which is
|
||||
# fine in most cases. If you want to harden your instance against CSRF attacks
|
||||
# and don't mind if some login-related things might break, you can set this
|
||||
# to 'strict' instead.
|
||||
#
|
||||
# For an overview of what this does, see:
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
|
||||
#
|
||||
# Options: ["lax", "strict"]
|
||||
# Default: "lax"
|
||||
advanced-cookies-samesite: "lax"
|
||||
|
|
2
go.sum
2
go.sum
|
@ -663,8 +663,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220524220425-1d687d428aca h1:xTaFYiPROfpPhqrfTIDXj0ri1SpfueYT951s4bAuDO8=
|
||||
golang.org/x/net v0.0.0-20220524220425-1d687d428aca/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
|
|
|
@ -23,12 +23,11 @@
|
|||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
)
|
||||
|
@ -61,58 +60,51 @@
|
|||
// description: "An OAuth2 access token for the newly-created account."
|
||||
// schema:
|
||||
// "$ref": "#/definitions/oauthToken"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal error
|
||||
// description: internal server error
|
||||
func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "accountCreatePOSTHandler")
|
||||
authed, err := oauth.Authed(c, true, true, false, false)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
l.Trace("parsing request form")
|
||||
form := &model.AccountCreateRequest{}
|
||||
if err := c.ShouldBind(form); err != nil || form == nil {
|
||||
l.Debugf("could not parse form from request: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
l.Tracef("validating form %+v", form)
|
||||
if err := validateCreateAccount(form); err != nil {
|
||||
l.Debugf("error validating form: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
l.Tracef("attempting to parse client ip address %s", clientIP)
|
||||
signUpIP := net.ParseIP(clientIP)
|
||||
if signUpIP == nil {
|
||||
l.Debugf("error validating sign up ip address %s", clientIP)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ip address could not be parsed from request"})
|
||||
err := errors.New("ip address could not be parsed from request")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
form.IP = signUpIP
|
||||
|
||||
ti, err := m.processor.AccountCreate(c.Request.Context(), authed, form)
|
||||
if err != nil {
|
||||
l.Errorf("internal server error while creating new account: %s", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
ti, errWithCode := m.processor.AccountCreate(c.Request.Context(), authed, form)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -122,6 +114,10 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
|||
// validateCreateAccount checks through all the necessary prerequisites for creating a new account,
|
||||
// according to the provided account create request. If the account isn't eligible, an error will be returned.
|
||||
func validateCreateAccount(form *model.AccountCreateRequest) error {
|
||||
if form == nil {
|
||||
return errors.New("form was nil")
|
||||
}
|
||||
|
||||
if !config.GetAccountsRegistrationOpen() {
|
||||
return errors.New("registration is not open for this server")
|
||||
}
|
||||
|
|
|
@ -19,12 +19,13 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -57,32 +58,35 @@
|
|||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountDeletePOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "AccountDeletePOSTHandler")
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
l.Tracef("retrieved account %+v", authed.Account.ID)
|
||||
|
||||
form := &model.AccountDeleteRequest{}
|
||||
if err := c.ShouldBind(&form); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if form.Password == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no password provided in account delete request"})
|
||||
err = errors.New("no password provided in account delete request")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
form.DeleteOriginID = authed.Account.ID
|
||||
|
||||
if errWithCode := m.processor.AccountDeleteLocal(c.Request.Context(), authed, form); errWithCode != nil {
|
||||
l.Debugf("could not delete account: %s", errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,11 +19,12 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -53,34 +54,38 @@
|
|||
// '200':
|
||||
// schema:
|
||||
// "$ref": "#/definitions/account"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetAcctID := c.Param(IDKey)
|
||||
if targetAcctID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||
err := errors.New("no account id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
acctInfo, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, targetAcctID)
|
||||
if err != nil {
|
||||
logrus.Debug(errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,15 +19,15 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -98,68 +98,67 @@
|
|||
// description: "The newly updated account."
|
||||
// schema:
|
||||
// "$ref": "#/definitions/account"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "accountUpdateCredentialsPATCHHandler")
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
l.Tracef("retrieved account %+v", authed.Account.ID)
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
form, err := parseUpdateAccountForm(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// if everything on the form is nil, then nothing has been set and we shouldn't continue
|
||||
if form.Discoverable == nil &&
|
||||
form.Bot == nil &&
|
||||
form.DisplayName == nil &&
|
||||
form.Note == nil &&
|
||||
form.Avatar == nil &&
|
||||
form.Header == nil &&
|
||||
form.Locked == nil &&
|
||||
form.Source.Privacy == nil &&
|
||||
form.Source.Sensitive == nil &&
|
||||
form.Source.Language == nil &&
|
||||
form.FieldsAttributes == nil {
|
||||
l.Debugf("could not parse form from request")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"})
|
||||
acctSensitive, errWithCode := m.processor.AccountUpdate(c.Request.Context(), authed, form)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
acctSensitive, err := m.processor.AccountUpdate(c.Request.Context(), authed, form)
|
||||
if err != nil {
|
||||
l.Debugf("could not update account: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
l.Tracef("conversion successful, returning OK and apisensitive account %+v", acctSensitive)
|
||||
c.JSON(http.StatusOK, acctSensitive)
|
||||
}
|
||||
|
||||
func parseUpdateAccountForm(c *gin.Context) (*model.UpdateCredentialsRequest, error) {
|
||||
// parse main fields from request
|
||||
form := &model.UpdateCredentialsRequest{
|
||||
Source: &model.UpdateSource{},
|
||||
}
|
||||
if err := c.ShouldBind(&form); err != nil || form == nil {
|
||||
|
||||
if err := c.ShouldBind(&form); err != nil {
|
||||
return nil, fmt.Errorf("could not parse form from request: %s", err)
|
||||
}
|
||||
|
||||
if form == nil ||
|
||||
(form.Discoverable == nil &&
|
||||
form.Bot == nil &&
|
||||
form.DisplayName == nil &&
|
||||
form.Note == nil &&
|
||||
form.Avatar == nil &&
|
||||
form.Header == nil &&
|
||||
form.Locked == nil &&
|
||||
form.Source.Privacy == nil &&
|
||||
form.Source.Sensitive == nil &&
|
||||
form.Source.Language == nil &&
|
||||
form.FieldsAttributes == nil) {
|
||||
return nil, errors.New("empty form submitted")
|
||||
}
|
||||
|
||||
// parse source field-by-field
|
||||
sourceMap := c.PostFormMap("source")
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/account"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
|
@ -65,7 +64,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler()
|
|||
|
||||
// check the response
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
suite.NoError(err)
|
||||
|
||||
// unmarshal the returned account
|
||||
apimodelAccount := &apimodel.Account{}
|
||||
|
@ -104,7 +103,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUnl
|
|||
|
||||
// check the response
|
||||
b1, err := ioutil.ReadAll(result1.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
suite.NoError(err)
|
||||
|
||||
// unmarshal the returned account
|
||||
apimodelAccount1 := &apimodel.Account{}
|
||||
|
@ -185,7 +184,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerGet
|
|||
|
||||
// check the response
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
suite.NoError(err)
|
||||
|
||||
// unmarshal the returned account
|
||||
apimodelAccount := &apimodel.Account{}
|
||||
|
@ -227,7 +226,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerTwo
|
|||
|
||||
// check the response
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
suite.NoError(err)
|
||||
|
||||
// unmarshal the returned account
|
||||
apimodelAccount := &apimodel.Account{}
|
||||
|
@ -271,7 +270,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerWit
|
|||
|
||||
// check the response
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
suite.NoError(err)
|
||||
|
||||
// unmarshal the returned account
|
||||
apimodelAccount := &apimodel.Account{}
|
||||
|
@ -313,8 +312,8 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerEmp
|
|||
|
||||
// check the response
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
suite.Equal(`{"error":"empty form submitted"}`, string(b))
|
||||
suite.NoError(err)
|
||||
suite.Equal(`{"error":"Bad Request: empty form submitted"}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateSource() {
|
||||
|
@ -348,7 +347,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpd
|
|||
|
||||
// check the response
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
suite.NoError(err)
|
||||
|
||||
// unmarshal the returned account
|
||||
apimodelAccount := &apimodel.Account{}
|
||||
|
|
|
@ -21,10 +21,9 @@
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -47,30 +46,31 @@
|
|||
// '200':
|
||||
// schema:
|
||||
// "$ref": "#/definitions/account"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountVerifyGETHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "accountVerifyGETHandler")
|
||||
authed, err := oauth.Authed(c, true, false, false, true)
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
acctSensitive, err := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID)
|
||||
if err != nil {
|
||||
l.Debugf("error getting account from processor: %s", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
||||
acctSensitive, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,10 +19,12 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -54,33 +56,38 @@
|
|||
// description: Your relationship to this account.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/accountRelationship"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountBlockPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetAcctID := c.Param(IDKey)
|
||||
if targetAcctID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||
err := errors.New("no account id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
relationship, errWithCode := m.processor.AccountBlockCreate(c.Request.Context(), authed, targetAcctID)
|
||||
if errWithCode != nil {
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,11 +19,13 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -75,39 +77,45 @@
|
|||
// description: Your relationship to this account.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/accountRelationship"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountFollowPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetAcctID := c.Param(IDKey)
|
||||
if targetAcctID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||
err := errors.New("no account id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
form := &model.AccountFollowRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
form.ID = targetAcctID
|
||||
|
||||
relationship, errWithCode := m.processor.AccountFollowCreate(c.Request.Context(), authed, form)
|
||||
if errWithCode != nil {
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,10 +19,12 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -56,33 +58,38 @@
|
|||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/account"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountFollowersGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetAcctID := c.Param(IDKey)
|
||||
if targetAcctID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||
err := errors.New("no account id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
followers, errWithCode := m.processor.AccountFollowersGet(c.Request.Context(), authed, targetAcctID)
|
||||
if errWithCode != nil {
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,10 +19,12 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -56,33 +58,38 @@
|
|||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/account"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountFollowingGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetAcctID := c.Param(IDKey)
|
||||
if targetAcctID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||
err := errors.New("no account id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
following, errWithCode := m.processor.AccountFollowingGet(c.Request.Context(), authed, targetAcctID)
|
||||
if errWithCode != nil {
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -43,24 +43,25 @@
|
|||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/accountRelationship"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "AccountRelationshipsGETHandler")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("error authing: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -69,8 +70,8 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
|
|||
// check fallback -- let's be generous and see if maybe it's just set as 'id'?
|
||||
id := c.Query("id")
|
||||
if id == "" {
|
||||
l.Debug("no account id specified in query")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||
err = errors.New("no account id(s) specified in query")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
targetAccountIDs = append(targetAccountIDs, id)
|
||||
|
@ -80,8 +81,8 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
|
|||
|
||||
for _, targetAccountID := range targetAccountIDs {
|
||||
r, errWithCode := m.processor.AccountRelationshipGet(c.Request.Context(), authed, targetAccountID)
|
||||
if err != nil {
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
relationships = append(relationships, *r)
|
||||
|
|
|
@ -19,13 +19,14 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -110,31 +111,32 @@
|
|||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/status"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "AccountStatusesGETHandler")
|
||||
|
||||
authed, err := oauth.Authed(c, false, false, false, false)
|
||||
if err != nil {
|
||||
l.Debugf("error authing: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetAcctID := c.Param(IDKey)
|
||||
if targetAcctID == "" {
|
||||
l.Debug("no account id specified in query")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||
err := errors.New("no account id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -143,8 +145,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
|||
if limitString != "" {
|
||||
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing limit string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
limit = int(i)
|
||||
|
@ -155,8 +157,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
|||
if excludeRepliesString != "" {
|
||||
i, err := strconv.ParseBool(excludeRepliesString)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing exclude replies string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude replies query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", ExcludeRepliesKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
excludeReplies = i
|
||||
|
@ -167,8 +169,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
|||
if excludeReblogsString != "" {
|
||||
i, err := strconv.ParseBool(excludeReblogsString)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing exclude reblogs string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude reblogs query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", ExcludeReblogsKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
excludeReblogs = i
|
||||
|
@ -191,8 +193,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
|||
if pinnedString != "" {
|
||||
i, err := strconv.ParseBool(pinnedString)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing pinned string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse pinned query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", PinnedKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
pinnedOnly = i
|
||||
|
@ -203,8 +205,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
|||
if mediaOnlyString != "" {
|
||||
i, err := strconv.ParseBool(mediaOnlyString)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing media only string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse media only query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", OnlyMediaKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
mediaOnly = i
|
||||
|
@ -215,19 +217,21 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
|||
if publicOnlyString != "" {
|
||||
i, err := strconv.ParseBool(publicOnlyString)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing public only string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse public only query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", OnlyPublicKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
publicOnly = i
|
||||
}
|
||||
|
||||
statuses, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
|
||||
resp, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error from processor account statuses get: %s", errWithCode)
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, statuses)
|
||||
if resp.LinkHeader != "" {
|
||||
c.Header("Link", resp.LinkHeader)
|
||||
}
|
||||
c.JSON(http.StatusOK, resp.Items)
|
||||
}
|
||||
|
|
|
@ -37,7 +37,47 @@ type AccountStatusesTestSuite struct {
|
|||
AccountStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *AccountStatusesTestSuite) TestGetStatusesMediaOnly() {
|
||||
func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnly() {
|
||||
// set up the request
|
||||
// we're getting statuses of admin
|
||||
targetAccount := suite.testAccounts["admin_account"]
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodGet, nil, fmt.Sprintf("/api/v1/accounts/%s/statuses?limit=20&only_media=false&only_public=true", targetAccount.ID), "")
|
||||
ctx.Params = gin.Params{
|
||||
gin.Param{
|
||||
Key: account.IDKey,
|
||||
Value: targetAccount.ID,
|
||||
},
|
||||
}
|
||||
|
||||
// call the handler
|
||||
suite.accountModule.AccountStatusesGETHandler(ctx)
|
||||
|
||||
// 1. we should have OK because our request was valid
|
||||
suite.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
// 2. we should have no error message in the result body
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
// check the response
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
// unmarshal the returned statuses
|
||||
apimodelStatuses := []*apimodel.Status{}
|
||||
err = json.Unmarshal(b, &apimodelStatuses)
|
||||
suite.NoError(err)
|
||||
suite.NotEmpty(apimodelStatuses)
|
||||
|
||||
for _, s := range apimodelStatuses {
|
||||
suite.Equal(apimodel.VisibilityPublic, s.Visibility)
|
||||
}
|
||||
|
||||
suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=false&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01G36SF3V6Y6V5BF9P4R7PQG7G&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=false&only_public=true>; rel="prev"`, result.Header.Get("link"))
|
||||
}
|
||||
|
||||
func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnlyMediaOnly() {
|
||||
// set up the request
|
||||
// we're getting statuses of admin
|
||||
targetAccount := suite.testAccounts["admin_account"]
|
||||
|
@ -74,6 +114,8 @@ func (suite *AccountStatusesTestSuite) TestGetStatusesMediaOnly() {
|
|||
suite.NotEmpty(s.MediaAttachments)
|
||||
suite.Equal(apimodel.VisibilityPublic, s.Visibility)
|
||||
}
|
||||
|
||||
suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=true&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=true&only_public=true>; rel="prev"`, result.Header.Get("link"))
|
||||
}
|
||||
|
||||
func TestAccountStatusesTestSuite(t *testing.T) {
|
||||
|
|
|
@ -19,10 +19,12 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -54,33 +56,38 @@
|
|||
// description: Your relationship to this account.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/accountRelationship"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetAcctID := c.Param(IDKey)
|
||||
if targetAcctID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||
err := errors.New("no account id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
relationship, errWithCode := m.processor.AccountBlockRemove(c.Request.Context(), authed, targetAcctID)
|
||||
if errWithCode != nil {
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,12 +19,12 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -56,37 +56,38 @@
|
|||
// description: Your relationship to this account.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/accountRelationship"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "AccountUnfollowPOSTHandler")
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debug(err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetAcctID := c.Param(IDKey)
|
||||
if targetAcctID == "" {
|
||||
l.Debug(err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||
err := errors.New("no account id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
relationship, errWithCode := m.processor.AccountFollowRemove(c.Request.Context(), authed, targetAcctID)
|
||||
if errWithCode != nil {
|
||||
l.Debug(errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,12 +19,14 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -72,53 +74,47 @@
|
|||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountActionPOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "AccountActionPOSTHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
|
||||
// make sure we're authed...
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// with an admin account
|
||||
if !authed.User.Admin {
|
||||
l.Debugf("user %s not an admin", authed.User.ID)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// extract the form from the request context
|
||||
l.Tracef("parsing request form: %+v", c.Request.Form)
|
||||
form := &model.AdminAccountActionRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
l.Debugf("error parsing form %+v: %s", c.Request.Form, err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if form.Type == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no type specified"})
|
||||
err := errors.New("no type specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetAcctID := c.Param(IDKey)
|
||||
if targetAcctID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||
err := errors.New("no account id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
form.TargetAccountID = targetAcctID
|
||||
|
||||
if errWithCode := m.processor.AdminAccountAction(c.Request.Context(), authed, form); errWithCode != nil {
|
||||
l.Debugf("error performing account action: %s", errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,21 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
|
@ -7,9 +25,9 @@
|
|||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -86,33 +104,33 @@
|
|||
// Note that if a list has been imported, then an `array` of newly created domain blocks will be returned instead.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainBlock"
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "DomainBlocksPOSTHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
|
||||
// make sure we're authed with an admin account
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if !authed.User.Admin {
|
||||
l.Debugf("user %s not an admin", authed.User.ID)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -121,49 +139,43 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) {
|
|||
if importString != "" {
|
||||
i, err := strconv.ParseBool(importString)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing import string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse import query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", ImportQueryKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
imp = i
|
||||
}
|
||||
|
||||
// extract the media create form from the request context
|
||||
l.Tracef("parsing request form: %+v", c.Request.Form)
|
||||
form := &model.DomainBlockCreateRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
l.Debugf("error parsing form %+v: %s", c.Request.Form, err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// Give the fields on the request form a first pass to make sure the request is superficially valid.
|
||||
l.Tracef("validating form %+v", form)
|
||||
if err := validateCreateDomainBlock(form, imp); err != nil {
|
||||
l.Debugf("error validating form: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
err := fmt.Errorf("error validating form: %s", err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if imp {
|
||||
// we're importing multiple blocks
|
||||
domainBlocks, err := m.processor.AdminDomainBlocksImport(c.Request.Context(), authed, form)
|
||||
if err != nil {
|
||||
l.Debugf("error importing domain blocks: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
domainBlocks, errWithCode := m.processor.AdminDomainBlocksImport(c.Request.Context(), authed, form)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, domainBlocks)
|
||||
} else {
|
||||
// we're just creating one block
|
||||
domainBlock, err := m.processor.AdminDomainBlockCreate(c.Request.Context(), authed, form)
|
||||
if err != nil {
|
||||
l.Debugf("error creating domain block: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, domainBlock)
|
||||
return
|
||||
}
|
||||
|
||||
// we're just creating one block
|
||||
domainBlock, errWithCode := m.processor.AdminDomainBlockCreate(c.Request.Context(), authed, form)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, domainBlock)
|
||||
}
|
||||
|
||||
func validateCreateDomainBlock(form *model.DomainBlockCreateRequest, imp bool) error {
|
||||
|
|
|
@ -1,11 +1,31 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -36,48 +56,46 @@
|
|||
// description: The domain block that was just deleted.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainBlock"
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainBlockDELETEHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "DomainBlockDELETEHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
|
||||
// make sure we're authed with an admin account
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if !authed.User.Admin {
|
||||
l.Debugf("user %s not an admin", authed.User.ID)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
domainBlockID := c.Param(IDKey)
|
||||
if domainBlockID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"})
|
||||
err := errors.New("no domain block id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
domainBlock, errWithCode := m.processor.AdminDomainBlockDelete(c.Request.Context(), authed, domainBlockID)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error deleting domain block: %s", errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,32 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -37,41 +57,40 @@
|
|||
// description: The requested domain block.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/domainBlock"
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainBlockGETHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "DomainBlockGETHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
|
||||
// make sure we're authed with an admin account
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if !authed.User.Admin {
|
||||
l.Debugf("user %s not an admin", authed.User.ID)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
domainBlockID := c.Param(IDKey)
|
||||
if domainBlockID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"})
|
||||
err := errors.New("no domain block id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -80,17 +99,16 @@ func (m *Module) DomainBlockGETHandler(c *gin.Context) {
|
|||
if exportString != "" {
|
||||
i, err := strconv.ParseBool(exportString)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing export string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
export = i
|
||||
}
|
||||
|
||||
domainBlock, err := m.processor.AdminDomainBlockGet(c.Request.Context(), authed, domainBlockID, export)
|
||||
if err != nil {
|
||||
l.Debugf("error getting domain block: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
domainBlock, errWithCode := m.processor.AdminDomainBlockGet(c.Request.Context(), authed, domainBlockID, export)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,31 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -43,35 +62,33 @@
|
|||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/domainBlock"
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) DomainBlocksGETHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "DomainBlocksGETHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
|
||||
// make sure we're authed with an admin account
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if !authed.User.Admin {
|
||||
l.Debugf("user %s not an admin", authed.User.ID)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -80,17 +97,16 @@ func (m *Module) DomainBlocksGETHandler(c *gin.Context) {
|
|||
if exportString != "" {
|
||||
i, err := strconv.ParseBool(exportString)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing export string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
export = i
|
||||
}
|
||||
|
||||
domainBlocks, err := m.processor.AdminDomainBlocksGet(c.Request.Context(), authed, export)
|
||||
if err != nil {
|
||||
l.Debugf("error getting domain blocks: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
domainBlocks, errWithCode := m.processor.AdminDomainBlocksGet(c.Request.Context(), authed, export)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -24,9 +24,9 @@
|
|||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
)
|
||||
|
@ -69,59 +69,52 @@
|
|||
// description: The newly-created emoji.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/emoji"
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict -- domain/shortcode combo for emoji already exists
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "emojiCreatePOSTHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
|
||||
// make sure we're authed with an admin account
|
||||
authed, err := oauth.Authed(c, true, true, true, true) // posting a status is serious business so we want *everything*
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if !authed.User.Admin {
|
||||
l.Debugf("user %s not an admin", authed.User.ID)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// extract the media create form from the request context
|
||||
l.Tracef("parsing request form: %+v", c.Request.Form)
|
||||
form := &model.EmojiCreateRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
l.Debugf("error parsing form %+v: %s", c.Request.Form, err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// Give the fields on the request form a first pass to make sure the request is superficially valid.
|
||||
l.Tracef("validating form %+v", form)
|
||||
if err := validateCreateEmoji(form); err != nil {
|
||||
l.Debugf("error validating form: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
apiEmoji, errWithCode := m.processor.AdminEmojiCreate(c.Request.Context(), authed, form)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error creating emoji: %s", errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -129,7 +122,6 @@ func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) {
|
|||
}
|
||||
|
||||
func validateCreateEmoji(form *model.EmojiCreateRequest) error {
|
||||
// check there actually is an image attached and it's not size 0
|
||||
if form.Image == nil || form.Image.Size == 0 {
|
||||
return errors.New("no emoji given")
|
||||
}
|
||||
|
|
|
@ -1,3 +1,21 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package admin_test
|
||||
|
||||
import (
|
||||
|
@ -120,7 +138,7 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateAlreadyExists() {
|
|||
suite.NoError(err)
|
||||
suite.NotEmpty(b)
|
||||
|
||||
suite.Equal(`{"error":"conflict: emoji with shortcode rainbow already exists"}`, string(b))
|
||||
suite.Equal(`{"error":"Conflict: emoji with shortcode rainbow already exists"}`, string(b))
|
||||
}
|
||||
|
||||
func TestEmojiCreateTestSuite(t *testing.T) {
|
||||
|
|
|
@ -23,9 +23,10 @@
|
|||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -54,39 +55,34 @@
|
|||
// '200':
|
||||
// description: |-
|
||||
// Echos the number of days requested. The cleanup is performed asynchronously after the request completes.
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "MediaCleanupPOSTHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
|
||||
// make sure we're authed...
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// with an admin account
|
||||
if !authed.User.Admin {
|
||||
l.Debugf("user %s not an admin", authed.User.ID)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// extract the form from the request context
|
||||
l.Tracef("parsing request form: %+v", c.Request.Form)
|
||||
form := &model.MediaCleanupRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
l.Debugf("error parsing form %+v: %s", c.Request.Form, err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -101,8 +97,7 @@ func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) {
|
|||
}
|
||||
|
||||
if errWithCode := m.processor.AdminMediaPrune(c.Request.Context(), remoteCacheDays); errWithCode != nil {
|
||||
l.Debugf("error starting prune of remote media: %s", errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -22,18 +22,16 @@
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// these consts are used to ensure users can't spam huge entries into our database
|
||||
const (
|
||||
// permitted length for most fields
|
||||
formFieldLen = 64
|
||||
// redirect can be a bit bigger because we probably need to encode data in the redirect uri
|
||||
formFieldLen = 64
|
||||
formRedirectLen = 512
|
||||
)
|
||||
|
||||
|
@ -64,56 +62,63 @@
|
|||
// description: "The newly-created application."
|
||||
// schema:
|
||||
// "$ref": "#/definitions/application"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '422':
|
||||
// description: unprocessable
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal error
|
||||
// description: internal server error
|
||||
func (m *Module) AppsPOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "AppsPOSTHandler")
|
||||
l.Trace("entering AppsPOSTHandler")
|
||||
|
||||
authed, err := oauth.Authed(c, false, false, false, false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
form := &model.ApplicationCreateRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// check lengths of fields before proceeding so the user can't spam huge entries into the database
|
||||
if len(form.ClientName) > formFieldLen {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("client_name must be less than %d bytes", formFieldLen)})
|
||||
return
|
||||
}
|
||||
if len(form.Website) > formFieldLen {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("website must be less than %d bytes", formFieldLen)})
|
||||
return
|
||||
}
|
||||
if len(form.RedirectURIs) > formRedirectLen {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("redirect_uris must be less than %d bytes", formRedirectLen)})
|
||||
return
|
||||
}
|
||||
if len(form.Scopes) > formFieldLen {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("scopes must be less than %d bytes", formFieldLen)})
|
||||
err := fmt.Errorf("client_name must be less than %d bytes", formFieldLen)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
apiApp, err := m.processor.AppCreate(c.Request.Context(), authed, form)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
if len(form.RedirectURIs) > formRedirectLen {
|
||||
err := fmt.Errorf("redirect_uris must be less than %d bytes", formRedirectLen)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if len(form.Scopes) > formFieldLen {
|
||||
err := fmt.Errorf("scopes must be less than %d bytes", formFieldLen)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if len(form.Website) > formFieldLen {
|
||||
err := fmt.Errorf("website must be less than %d bytes", formFieldLen)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
apiApp, errWithCode := m.processor.AppCreate(c.Request.Context(), authed, form)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oidc"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||
)
|
||||
|
||||
|
@ -66,17 +66,17 @@
|
|||
|
||||
// Module implements the ClientAPIModule interface for
|
||||
type Module struct {
|
||||
db db.DB
|
||||
server oauth.Server
|
||||
idp oidc.IDP
|
||||
db db.DB
|
||||
idp oidc.IDP
|
||||
processor processing.Processor
|
||||
}
|
||||
|
||||
// New returns a new auth module
|
||||
func New(db db.DB, server oauth.Server, idp oidc.IDP) api.ClientModule {
|
||||
func New(db db.DB, idp oidc.IDP, processor processing.Processor) api.ClientModule {
|
||||
return &Module{
|
||||
db: db,
|
||||
server: server,
|
||||
idp: idp,
|
||||
db: db,
|
||||
idp: idp,
|
||||
processor: processor,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,29 +19,42 @@
|
|||
package auth_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
|
||||
"codeberg.org/gruf/go-store/kv"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/memstore"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/auth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oidc"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type AuthStandardTestSuite struct {
|
||||
suite.Suite
|
||||
db db.DB
|
||||
idp oidc.IDP
|
||||
oauthServer oauth.Server
|
||||
db db.DB
|
||||
storage *kv.KVStore
|
||||
mediaManager media.Manager
|
||||
federator federation.Federator
|
||||
processor processing.Processor
|
||||
emailSender email.Sender
|
||||
idp oidc.IDP
|
||||
oauthServer oauth.Server
|
||||
|
||||
// standard suite models
|
||||
testTokens map[string]*gtsmodel.Token
|
||||
|
@ -69,39 +82,53 @@ func (suite *AuthStandardTestSuite) SetupSuite() {
|
|||
|
||||
func (suite *AuthStandardTestSuite) SetupTest() {
|
||||
testrig.InitTestConfig()
|
||||
suite.db = testrig.NewTestDB()
|
||||
testrig.InitTestLog()
|
||||
|
||||
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||
|
||||
suite.db = testrig.NewTestDB()
|
||||
suite.storage = testrig.NewTestStorage()
|
||||
suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage)
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker)
|
||||
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
|
||||
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker)
|
||||
|
||||
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
|
||||
var err error
|
||||
suite.idp, err = oidc.NewIDP(context.Background())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
suite.authModule = auth.New(suite.db, suite.oauthServer, suite.idp).(*auth.Module)
|
||||
testrig.StandardDBSetup(suite.db, nil)
|
||||
suite.authModule = auth.New(suite.db, suite.idp, suite.processor).(*auth.Module)
|
||||
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
||||
}
|
||||
|
||||
func (suite *AuthStandardTestSuite) TearDownTest() {
|
||||
testrig.StandardDBTeardown(suite.db)
|
||||
}
|
||||
|
||||
func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath string) (*gin.Context, *httptest.ResponseRecorder) {
|
||||
func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath string, requestBody []byte, bodyContentType string) (*gin.Context, *httptest.ResponseRecorder) {
|
||||
// create the recorder and gin test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, engine := gin.CreateTestContext(recorder)
|
||||
|
||||
// load templates into the engine
|
||||
testrig.ConfigureTemplatesWithGin(engine)
|
||||
testrig.ConfigureTemplatesWithGin(engine, "../../../../web/template")
|
||||
|
||||
// create the request
|
||||
protocol := config.GetProtocol()
|
||||
host := config.GetHost()
|
||||
baseURI := fmt.Sprintf("%s://%s", protocol, host)
|
||||
requestURI := fmt.Sprintf("%s/%s", baseURI, requestPath)
|
||||
ctx.Request = httptest.NewRequest(requestMethod, requestURI, nil) // the endpoint we're hitting
|
||||
|
||||
ctx.Request = httptest.NewRequest(requestMethod, requestURI, bytes.NewReader(requestBody)) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "text/html")
|
||||
|
||||
if bodyContentType != "" {
|
||||
ctx.Request.Header.Set("Content-Type", bodyContentType)
|
||||
}
|
||||
|
||||
// trigger the session middleware on the context
|
||||
store := memstore.NewStore(make([]byte, 32), make([]byte, 32))
|
||||
store.Options(router.SessionOptions())
|
||||
|
|
|
@ -23,9 +23,6 @@
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -33,18 +30,22 @@
|
|||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// helpfulAdvice is a handy hint to users;
|
||||
// particularly important during the login flow
|
||||
var helpfulAdvice = "If you arrived at this error during a login/oauth flow, please try clearing your session cookies and logging in again; if problems persist, make sure you're using the correct credentials"
|
||||
|
||||
// AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize
|
||||
// The idea here is to present an oauth authorize page to the user, with a button
|
||||
// that they have to click to accept.
|
||||
func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "AuthorizeGETHandler")
|
||||
s := sessions.Default(c)
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil {
|
||||
c.HTML(http.StatusNotAcceptable, "error.tmpl", gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -52,56 +53,75 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
|||
// If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page.
|
||||
userID, ok := s.Get(sessionUserID).(string)
|
||||
if !ok || userID == "" {
|
||||
l.Trace("userid was empty, parsing form then redirecting to sign in page")
|
||||
form := &model.OAuthAuthorize{}
|
||||
if err := c.Bind(form); err != nil {
|
||||
l.Debugf("invalid auth form: %s", err)
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
m.clearSession(s)
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
l.Debugf("parsed auth form: %+v", form)
|
||||
|
||||
if err := extractAuthForm(s, form); err != nil {
|
||||
l.Debugf(fmt.Sprintf("error parsing form at /oauth/authorize: %s", err))
|
||||
if errWithCode := saveAuthFormToSession(s, form); errWithCode != nil {
|
||||
m.clearSession(s)
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusSeeOther, AuthSignInPath)
|
||||
return
|
||||
}
|
||||
|
||||
// We can use the client_id on the session to retrieve info about the app associated with the client_id
|
||||
// use session information to validate app, user, and account for this request
|
||||
clientID, ok := s.Get(sessionClientID).(string)
|
||||
if !ok || clientID == "" {
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no client_id found in session"})
|
||||
return
|
||||
}
|
||||
app := >smodel.Application{}
|
||||
if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
|
||||
m.clearSession(s)
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
|
||||
"error": fmt.Sprintf("no application found for client id %s", clientID),
|
||||
})
|
||||
err := fmt.Errorf("key %s was not found in session", sessionClientID)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
app := >smodel.Application{}
|
||||
if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
|
||||
m.clearSession(s)
|
||||
safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID)
|
||||
var errWithCode gtserror.WithCode
|
||||
if err == db.ErrNoEntries {
|
||||
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
|
||||
} else {
|
||||
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
|
||||
}
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// redirect the user if they have not confirmed their email yet, thier account has not been approved yet,
|
||||
// or thier account has been disabled.
|
||||
user := >smodel.User{}
|
||||
if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil {
|
||||
m.clearSession(s)
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()})
|
||||
safe := fmt.Sprintf("user with id %s could not be retrieved", userID)
|
||||
var errWithCode gtserror.WithCode
|
||||
if err == db.ErrNoEntries {
|
||||
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
|
||||
} else {
|
||||
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
|
||||
}
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID)
|
||||
if err != nil {
|
||||
m.clearSession(s)
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()})
|
||||
safe := fmt.Sprintf("account with id %s could not be retrieved", user.AccountID)
|
||||
var errWithCode gtserror.WithCode
|
||||
if err == db.ErrNoEntries {
|
||||
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
|
||||
} else {
|
||||
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
|
||||
}
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
if !ensureUserIsAuthorizedOrRedirect(c, user, acct) {
|
||||
|
||||
if ensureUserIsAuthorizedOrRedirect(c, user, acct) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -109,25 +129,27 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
|||
redirect, ok := s.Get(sessionRedirectURI).(string)
|
||||
if !ok || redirect == "" {
|
||||
m.clearSession(s)
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no redirect_uri found in session"})
|
||||
err := fmt.Errorf("key %s was not found in session", sessionRedirectURI)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
scope, ok := s.Get(sessionScope).(string)
|
||||
if !ok || scope == "" {
|
||||
m.clearSession(s)
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no scope found in session"})
|
||||
err := fmt.Errorf("key %s was not found in session", sessionScope)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// the authorize template will display a form to the user where they can get some information
|
||||
// about the app that's trying to authorize, and the scope of the request.
|
||||
// They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler
|
||||
l.Trace("serving authorize html")
|
||||
c.HTML(http.StatusOK, "authorize.tmpl", gin.H{
|
||||
"appname": app.Name,
|
||||
"appwebsite": app.Website,
|
||||
"redirect": redirect,
|
||||
sessionScope: scope,
|
||||
"scope": scope,
|
||||
"user": acct.Username,
|
||||
})
|
||||
}
|
||||
|
@ -136,13 +158,10 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
|||
// At this point we assume that the user has A) logged in and B) accepted that the app should act for them,
|
||||
// so we should proceed with the authentication flow and generate an oauth token for them if we can.
|
||||
func (m *Module) AuthorizePOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "AuthorizePOSTHandler")
|
||||
s := sessions.Default(c)
|
||||
|
||||
// We need to retrieve the original form submitted to the authorizeGEThandler, and
|
||||
// recreate it on the request so that it can be used further by the oauth2 library.
|
||||
// So first fetch all the values from the session.
|
||||
|
||||
errs := []string{}
|
||||
|
||||
forceLogin, ok := s.Get(sessionForceLogin).(string)
|
||||
|
@ -152,77 +171,107 @@ func (m *Module) AuthorizePOSTHandler(c *gin.Context) {
|
|||
|
||||
responseType, ok := s.Get(sessionResponseType).(string)
|
||||
if !ok || responseType == "" {
|
||||
errs = append(errs, "session missing response_type")
|
||||
errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionResponseType))
|
||||
}
|
||||
|
||||
clientID, ok := s.Get(sessionClientID).(string)
|
||||
if !ok || clientID == "" {
|
||||
errs = append(errs, "session missing client_id")
|
||||
errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionClientID))
|
||||
}
|
||||
|
||||
redirectURI, ok := s.Get(sessionRedirectURI).(string)
|
||||
if !ok || redirectURI == "" {
|
||||
errs = append(errs, "session missing redirect_uri")
|
||||
errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionRedirectURI))
|
||||
}
|
||||
|
||||
scope, ok := s.Get(sessionScope).(string)
|
||||
if !ok {
|
||||
errs = append(errs, "session missing scope")
|
||||
errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionScope))
|
||||
}
|
||||
|
||||
userID, ok := s.Get(sessionUserID).(string)
|
||||
if !ok {
|
||||
errs = append(errs, "session missing userid")
|
||||
errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionUserID))
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
errs = append(errs, helpfulAdvice)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(errors.New("one or more missing keys on session during AuthorizePOSTHandler"), errs...), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// redirect the user if they have not confirmed their email yet, thier account has not been approved yet,
|
||||
// or thier account has been disabled.
|
||||
user := >smodel.User{}
|
||||
if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil {
|
||||
m.clearSession(s)
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()})
|
||||
safe := fmt.Sprintf("user with id %s could not be retrieved", userID)
|
||||
var errWithCode gtserror.WithCode
|
||||
if err == db.ErrNoEntries {
|
||||
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
|
||||
} else {
|
||||
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
|
||||
}
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID)
|
||||
if err != nil {
|
||||
m.clearSession(s)
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !ensureUserIsAuthorizedOrRedirect(c, user, acct) {
|
||||
safe := fmt.Sprintf("account with id %s could not be retrieved", user.AccountID)
|
||||
var errWithCode gtserror.WithCode
|
||||
if err == db.ErrNoEntries {
|
||||
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
|
||||
} else {
|
||||
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
|
||||
}
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if ensureUserIsAuthorizedOrRedirect(c, user, acct) {
|
||||
return
|
||||
}
|
||||
|
||||
// we're done with the session now, so just clear it out
|
||||
m.clearSession(s)
|
||||
|
||||
if len(errs) != 0 {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": strings.Join(errs, ": ")})
|
||||
return
|
||||
// we have to set the values on the request form
|
||||
// so that they're picked up by the oauth server
|
||||
c.Request.Form = url.Values{
|
||||
sessionForceLogin: {forceLogin},
|
||||
sessionResponseType: {responseType},
|
||||
sessionClientID: {clientID},
|
||||
sessionRedirectURI: {redirectURI},
|
||||
sessionScope: {scope},
|
||||
sessionUserID: {userID},
|
||||
}
|
||||
|
||||
// now set the values on the request
|
||||
values := url.Values{}
|
||||
values.Set(sessionForceLogin, forceLogin)
|
||||
values.Set(sessionResponseType, responseType)
|
||||
values.Set(sessionClientID, clientID)
|
||||
values.Set(sessionRedirectURI, redirectURI)
|
||||
values.Set(sessionScope, scope)
|
||||
values.Set(sessionUserID, userID)
|
||||
c.Request.Form = values
|
||||
l.Tracef("values on request set to %+v", c.Request.Form)
|
||||
|
||||
// and proceed with authorization using the oauth2 library
|
||||
if err := m.server.HandleAuthorizeRequest(c.Writer, c.Request); err != nil {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()})
|
||||
if err := m.processor.OAuthHandleAuthorizeRequest(c.Writer, c.Request); err != nil {
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice), m.processor.InstanceGet)
|
||||
}
|
||||
}
|
||||
|
||||
// extractAuthForm checks the given OAuthAuthorize form, and stores
|
||||
// the values in the form into the session.
|
||||
func extractAuthForm(s sessions.Session, form *model.OAuthAuthorize) error {
|
||||
// these fields are *required* so check 'em
|
||||
if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" {
|
||||
return errors.New("missing one of: response_type, client_id or redirect_uri")
|
||||
// saveAuthFormToSession checks the given OAuthAuthorize form,
|
||||
// and stores the values in the form into the session.
|
||||
func saveAuthFormToSession(s sessions.Session, form *model.OAuthAuthorize) gtserror.WithCode {
|
||||
if form == nil {
|
||||
err := errors.New("OAuthAuthorize form was nil")
|
||||
return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
|
||||
}
|
||||
|
||||
if form.ResponseType == "" {
|
||||
err := errors.New("field response_type was not set on OAuthAuthorize form")
|
||||
return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
|
||||
}
|
||||
|
||||
if form.ClientID == "" {
|
||||
err := errors.New("field client_id was not set on OAuthAuthorize form")
|
||||
return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
|
||||
}
|
||||
|
||||
if form.RedirectURI == "" {
|
||||
err := errors.New("field redirect_uri was not set on OAuthAuthorize form")
|
||||
return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
|
||||
}
|
||||
|
||||
// set default scope to read
|
||||
|
@ -237,29 +286,33 @@ func extractAuthForm(s sessions.Session, form *model.OAuthAuthorize) error {
|
|||
s.Set(sessionRedirectURI, form.RedirectURI)
|
||||
s.Set(sessionScope, form.Scope)
|
||||
s.Set(sessionState, uuid.NewString())
|
||||
return s.Save()
|
||||
|
||||
if err := s.Save(); err != nil {
|
||||
err := fmt.Errorf("error saving form values onto session: %s", err)
|
||||
return gtserror.NewErrorInternalError(err, helpfulAdvice)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureUserIsAuthorizedOrRedirect(ctx *gin.Context, user *gtsmodel.User, account *gtsmodel.Account) bool {
|
||||
func ensureUserIsAuthorizedOrRedirect(ctx *gin.Context, user *gtsmodel.User, account *gtsmodel.Account) (redirected bool) {
|
||||
if user.ConfirmedAt.IsZero() {
|
||||
ctx.Redirect(http.StatusSeeOther, CheckYourEmailPath)
|
||||
return false
|
||||
redirected = true
|
||||
return
|
||||
}
|
||||
|
||||
if !user.Approved {
|
||||
ctx.Redirect(http.StatusSeeOther, WaitForApprovalPath)
|
||||
return false
|
||||
redirected = true
|
||||
return
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
if user.Disabled || !account.SuspendedAt.IsZero() {
|
||||
ctx.Redirect(http.StatusSeeOther, AccountDisabledPath)
|
||||
return false
|
||||
redirected = true
|
||||
return
|
||||
}
|
||||
|
||||
if !account.SuspendedAt.IsZero() {
|
||||
ctx.Redirect(http.StatusSeeOther, AccountDisabledPath)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ func (suite *AuthAuthorizeTestSuite) TestAccountAuthorizeHandler() {
|
|||
}
|
||||
|
||||
doTest := func(testCase authorizeHandlerTestCase) {
|
||||
ctx, recorder := suite.newContext(http.MethodGet, auth.OauthAuthorizePath)
|
||||
ctx, recorder := suite.newContext(http.MethodGet, auth.OauthAuthorizePath, nil, "")
|
||||
|
||||
user := suite.testUsers["unconfirmed_account"]
|
||||
account := suite.testAccounts["unconfirmed_account"]
|
||||
|
|
|
@ -30,7 +30,9 @@
|
|||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oidc"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
|
@ -40,11 +42,14 @@
|
|||
func (m *Module) CallbackGETHandler(c *gin.Context) {
|
||||
s := sessions.Default(c)
|
||||
|
||||
// first make sure the state set in the cookie is the same as the state returned from the external provider
|
||||
// check the query vs session state parameter to mitigate csrf
|
||||
// https://auth0.com/docs/secure/attack-protection/state-parameters
|
||||
|
||||
state := c.Query(callbackStateParam)
|
||||
if state == "" {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "state query not found on callback"})
|
||||
err := fmt.Errorf("%s parameter not found on callback query", callbackStateParam)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -52,84 +57,104 @@ func (m *Module) CallbackGETHandler(c *gin.Context) {
|
|||
savedState, ok := savedStateI.(string)
|
||||
if !ok {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "state not found in session"})
|
||||
err := fmt.Errorf("key %s was not found in session", sessionState)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if state != savedState {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "state mismatch"})
|
||||
err := errors.New("mismatch between query state and session state")
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve stored claims using code
|
||||
code := c.Query(callbackCodeParam)
|
||||
|
||||
claims, err := m.idp.HandleCallback(c.Request.Context(), code)
|
||||
if err != nil {
|
||||
if code == "" {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
err := fmt.Errorf("%s parameter not found on callback query", callbackCodeParam)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// We can use the client_id on the session to retrieve info about the app associated with the client_id
|
||||
claims, errWithCode := m.idp.HandleCallback(c.Request.Context(), code)
|
||||
if errWithCode != nil {
|
||||
m.clearSession(s)
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// We can use the client_id on the session to retrieve
|
||||
// info about the app associated with the client_id
|
||||
clientID, ok := s.Get(sessionClientID).(string)
|
||||
if !ok || clientID == "" {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "no client_id found in session during callback"})
|
||||
return
|
||||
}
|
||||
app := >smodel.Application{}
|
||||
if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("no application found for client id %s", clientID)})
|
||||
err := fmt.Errorf("key %s was not found in session", sessionClientID)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := m.parseUserFromClaims(c.Request.Context(), claims, net.IP(c.ClientIP()), app.ID)
|
||||
if err != nil {
|
||||
app := >smodel.Application{}
|
||||
if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID)
|
||||
var errWithCode gtserror.WithCode
|
||||
if err == db.ErrNoEntries {
|
||||
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
|
||||
} else {
|
||||
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
|
||||
}
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
user, errWithCode := m.parseUserFromClaims(c.Request.Context(), claims, net.IP(c.ClientIP()), app.ID)
|
||||
if errWithCode != nil {
|
||||
m.clearSession(s)
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
s.Set(sessionUserID, user.ID)
|
||||
if err := s.Save(); err != nil {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, OauthAuthorizePath)
|
||||
}
|
||||
|
||||
func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, ip net.IP, appID string) (*gtsmodel.User, error) {
|
||||
func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, ip net.IP, appID string) (*gtsmodel.User, gtserror.WithCode) {
|
||||
if claims.Email == "" {
|
||||
return nil, errors.New("no email returned in claims")
|
||||
err := errors.New("no email returned in claims")
|
||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
|
||||
// see if we already have a user for this email address
|
||||
// if so, we don't need to continue + create one
|
||||
user := >smodel.User{}
|
||||
err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: claims.Email}}, user)
|
||||
if err == nil {
|
||||
// we do! so we can just return it
|
||||
return user, nil
|
||||
}
|
||||
|
||||
if err != db.ErrNoEntries {
|
||||
// we have an actual error in the database
|
||||
return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
|
||||
err := fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// maybe we have an unconfirmed user
|
||||
err = m.db.GetWhere(ctx, []db.Where{{Key: "unconfirmed_email", Value: claims.Email}}, user)
|
||||
if err == nil {
|
||||
// user is unconfirmed so return an error
|
||||
return nil, fmt.Errorf("user with email address %s is unconfirmed", claims.Email)
|
||||
err := fmt.Errorf("user with email address %s is unconfirmed", claims.Email)
|
||||
return nil, gtserror.NewErrorForbidden(err, err.Error())
|
||||
}
|
||||
|
||||
if err != db.ErrNoEntries {
|
||||
// we have an actual error in the database
|
||||
return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
|
||||
err := fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// we don't have a confirmed or unconfirmed user with the claimed email address
|
||||
|
@ -138,10 +163,10 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
|
|||
// check if the email address is available for use; if it's not there's nothing we can so
|
||||
emailAvailable, err := m.db.IsEmailAvailable(ctx, claims.Email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("email %s not available: %s", claims.Email, err)
|
||||
return nil, gtserror.NewErrorBadRequest(err)
|
||||
}
|
||||
if !emailAvailable {
|
||||
return nil, fmt.Errorf("email %s in use", claims.Email)
|
||||
return nil, gtserror.NewErrorConflict(fmt.Errorf("email address %s is not available", claims.Email))
|
||||
}
|
||||
|
||||
// now we need a username
|
||||
|
@ -149,12 +174,12 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
|
|||
|
||||
// make sure claims.Name is defined since we'll be using that for the username
|
||||
if claims.Name == "" {
|
||||
return nil, errors.New("no name returned in claims")
|
||||
err := errors.New("no name returned in claims")
|
||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
|
||||
// check if we can just use claims.Name as-is
|
||||
err = validate.Username(claims.Name)
|
||||
if err == nil {
|
||||
if err = validate.Username(claims.Name); err == nil {
|
||||
// the name we have on the claims is already a valid username
|
||||
username = claims.Name
|
||||
} else {
|
||||
|
@ -166,12 +191,12 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
|
|||
// lowercase the whole thing
|
||||
lower := strings.ToLower(underscored)
|
||||
// see if this is valid....
|
||||
if err := validate.Username(lower); err == nil {
|
||||
// we managed to get a valid username
|
||||
username = lower
|
||||
} else {
|
||||
return nil, fmt.Errorf("couldn't parse a valid username from claims.Name value of %s", claims.Name)
|
||||
if err := validate.Username(lower); err != nil {
|
||||
err := fmt.Errorf("couldn't parse a valid username from claims.Name value of %s: %s", claims.Name, err)
|
||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
// we managed to get a valid username
|
||||
username = lower
|
||||
}
|
||||
|
||||
var iString string
|
||||
|
@ -185,7 +210,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
|
|||
for i := 1; !found; i++ {
|
||||
usernameAvailable, err := m.db.IsUsernameAvailable(ctx, username+iString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if usernameAvailable {
|
||||
// no error so we've found a username that works
|
||||
|
@ -223,7 +248,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
|
|||
// create the user! this will also create an account and store it in the database so we don't need to do that here
|
||||
user, err = m.db.NewSignup(ctx, username, "", requireApproval, claims.Email, password, ip, "", appID, emailVerified, admin)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating user: %s", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
|
|
|
@ -21,14 +21,14 @@
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
@ -41,64 +41,62 @@ type login struct {
|
|||
|
||||
// SignInGETHandler should be served at https://example.org/auth/sign_in.
|
||||
// The idea is to present a sign in page to the user, where they can enter their username and password.
|
||||
// The form will then POST to the sign in page, which will be handled by SignInPOSTHandler
|
||||
// The form will then POST to the sign in page, which will be handled by SignInPOSTHandler.
|
||||
// If an idp provider is set, then the user will be redirected to that to do their sign in.
|
||||
func (m *Module) SignInGETHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "SignInGETHandler")
|
||||
l.Trace("entering sign in handler")
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if m.idp != nil {
|
||||
s := sessions.Default(c)
|
||||
|
||||
stateI := s.Get(sessionState)
|
||||
state, ok := stateI.(string)
|
||||
if !ok {
|
||||
m.clearSession(s)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "state not found in session"})
|
||||
return
|
||||
}
|
||||
|
||||
redirect := m.idp.AuthCodeURL(state)
|
||||
l.Debugf("redirecting to external idp at %s", redirect)
|
||||
c.Redirect(http.StatusSeeOther, redirect)
|
||||
if m.idp == nil {
|
||||
// no idp provider, use our own funky little sign in page
|
||||
c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{})
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{})
|
||||
|
||||
// idp provider is in use, so redirect to it
|
||||
s := sessions.Default(c)
|
||||
|
||||
stateI := s.Get(sessionState)
|
||||
state, ok := stateI.(string)
|
||||
if !ok {
|
||||
m.clearSession(s)
|
||||
err := fmt.Errorf("key %s was not found in session", sessionState)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusSeeOther, m.idp.AuthCodeURL(state))
|
||||
}
|
||||
|
||||
// SignInPOSTHandler should be served at https://example.org/auth/sign_in.
|
||||
// The idea is to present a sign in page to the user, where they can enter their username and password.
|
||||
// The handler will then redirect to the auth handler served at /auth
|
||||
func (m *Module) SignInPOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "SignInPOSTHandler")
|
||||
s := sessions.Default(c)
|
||||
|
||||
form := &login{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
m.clearSession(s)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
l.Tracef("parsed form: %+v", form)
|
||||
|
||||
userid, err := m.ValidatePassword(c.Request.Context(), form.Email, form.Password)
|
||||
if err != nil {
|
||||
c.String(http.StatusForbidden, err.Error())
|
||||
m.clearSession(s)
|
||||
userid, errWithCode := m.ValidatePassword(c.Request.Context(), form.Email, form.Password)
|
||||
if errWithCode != nil {
|
||||
// don't clear session here, so the user can just press back and try again
|
||||
// if they accidentally gave the wrong password or something
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
s.Set(sessionUserID, userid)
|
||||
if err := s.Save(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
m.clearSession(s)
|
||||
return
|
||||
err := fmt.Errorf("error saving user id onto session: %s", err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorInternalError(err, helpfulAdvice), m.processor.InstanceGet)
|
||||
}
|
||||
|
||||
l.Trace("redirecting to auth page")
|
||||
c.Redirect(http.StatusFound, OauthAuthorizePath)
|
||||
}
|
||||
|
||||
|
@ -106,42 +104,34 @@ func (m *Module) SignInPOSTHandler(c *gin.Context) {
|
|||
// The goal is to authenticate the password against the one for that email
|
||||
// address stored in the database. If OK, we return the userid (a ulid) for that user,
|
||||
// so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db.
|
||||
func (m *Module) ValidatePassword(ctx context.Context, email string, password string) (userid string, err error) {
|
||||
l := logrus.WithField("func", "ValidatePassword")
|
||||
|
||||
// make sure an email/password was provided and bail if not
|
||||
func (m *Module) ValidatePassword(ctx context.Context, email string, password string) (string, gtserror.WithCode) {
|
||||
if email == "" || password == "" {
|
||||
l.Debug("email or password was not provided")
|
||||
return incorrectPassword()
|
||||
err := errors.New("email or password was not provided")
|
||||
return incorrectPassword(err)
|
||||
}
|
||||
|
||||
// first we select the user from the database based on email address, bail if no user found for that email
|
||||
gtsUser := >smodel.User{}
|
||||
|
||||
if err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: email}}, gtsUser); err != nil {
|
||||
l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err)
|
||||
return incorrectPassword()
|
||||
user := >smodel.User{}
|
||||
if err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: email}}, user); err != nil {
|
||||
err := fmt.Errorf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err)
|
||||
return incorrectPassword(err)
|
||||
}
|
||||
|
||||
// make sure a password is actually set and bail if not
|
||||
if gtsUser.EncryptedPassword == "" {
|
||||
l.Warnf("encrypted password for user %s was empty for some reason", gtsUser.Email)
|
||||
return incorrectPassword()
|
||||
if user.EncryptedPassword == "" {
|
||||
err := fmt.Errorf("encrypted password for user %s was empty for some reason", user.Email)
|
||||
return incorrectPassword(err)
|
||||
}
|
||||
|
||||
// compare the provided password with the encrypted one from the db, bail if they don't match
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(gtsUser.EncryptedPassword), []byte(password)); err != nil {
|
||||
l.Debugf("password hash didn't match for user %s during login attempt: %s", gtsUser.Email, err)
|
||||
return incorrectPassword()
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(password)); err != nil {
|
||||
err := fmt.Errorf("password hash didn't match for user %s during login attempt: %s", user.Email, err)
|
||||
return incorrectPassword(err)
|
||||
}
|
||||
|
||||
// If we've made it this far the email/password is correct, so we can just return the id of the user.
|
||||
userid = gtsUser.ID
|
||||
l.Tracef("returning (%s, %s)", userid, err)
|
||||
return
|
||||
return user.ID, nil
|
||||
}
|
||||
|
||||
// incorrectPassword is just a little helper function to use in the ValidatePassword function
|
||||
func incorrectPassword() (string, error) {
|
||||
return "", errors.New("password/email combination was incorrect")
|
||||
// incorrectPassword wraps the given error in a gtserror.WithCode, and returns
|
||||
// only a generic 'safe' error message to the user, to not give any info away.
|
||||
func incorrectPassword(err error) (string, gtserror.WithCode) {
|
||||
safeErr := fmt.Errorf("password/email combination was incorrect")
|
||||
return "", gtserror.NewErrorUnauthorized(err, safeErr.Error(), helpfulAdvice)
|
||||
}
|
||||
|
|
|
@ -22,56 +22,94 @@
|
|||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type tokenBody struct {
|
||||
type tokenRequestForm struct {
|
||||
GrantType *string `form:"grant_type" json:"grant_type" xml:"grant_type"`
|
||||
Code *string `form:"code" json:"code" xml:"code"`
|
||||
RedirectURI *string `form:"redirect_uri" json:"redirect_uri" xml:"redirect_uri"`
|
||||
ClientID *string `form:"client_id" json:"client_id" xml:"client_id"`
|
||||
ClientSecret *string `form:"client_secret" json:"client_secret" xml:"client_secret"`
|
||||
Code *string `form:"code" json:"code" xml:"code"`
|
||||
GrantType *string `form:"grant_type" json:"grant_type" xml:"grant_type"`
|
||||
RedirectURI *string `form:"redirect_uri" json:"redirect_uri" xml:"redirect_uri"`
|
||||
Scope *string `form:"scope" json:"scope" xml:"scope"`
|
||||
}
|
||||
|
||||
// TokenPOSTHandler should be served as a POST at https://example.org/oauth/token
|
||||
// The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs.
|
||||
func (m *Module) TokenPOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "TokenPOSTHandler")
|
||||
l.Trace("entered TokenPOSTHandler")
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
form := &tokenBody{}
|
||||
if err := c.ShouldBind(form); err == nil {
|
||||
c.Request.Form = url.Values{}
|
||||
if form.ClientID != nil {
|
||||
c.Request.Form.Set("client_id", *form.ClientID)
|
||||
}
|
||||
if form.ClientSecret != nil {
|
||||
c.Request.Form.Set("client_secret", *form.ClientSecret)
|
||||
}
|
||||
if form.Code != nil {
|
||||
c.Request.Form.Set("code", *form.Code)
|
||||
}
|
||||
if form.GrantType != nil {
|
||||
c.Request.Form.Set("grant_type", *form.GrantType)
|
||||
}
|
||||
if form.RedirectURI != nil {
|
||||
c.Request.Form.Set("redirect_uri", *form.RedirectURI)
|
||||
}
|
||||
if form.Scope != nil {
|
||||
c.Request.Form.Set("scope", *form.Scope)
|
||||
}
|
||||
help := []string{}
|
||||
|
||||
form := &tokenRequestForm{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
api.OAuthErrorHandler(c, gtserror.NewErrorBadRequest(oauth.InvalidRequest(), err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.server.HandleTokenRequest(c.Writer, c.Request); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
c.Request.Form = url.Values{}
|
||||
|
||||
var grantType string
|
||||
if form.GrantType != nil {
|
||||
grantType = *form.GrantType
|
||||
c.Request.Form.Set("grant_type", grantType)
|
||||
} else {
|
||||
help = append(help, "grant_type was not set in the token request form, but must be set to authorization_code or client_credentials")
|
||||
}
|
||||
|
||||
if form.ClientID != nil {
|
||||
c.Request.Form.Set("client_id", *form.ClientID)
|
||||
} else {
|
||||
help = append(help, "client_id was not set in the token request form")
|
||||
}
|
||||
|
||||
if form.ClientSecret != nil {
|
||||
c.Request.Form.Set("client_secret", *form.ClientSecret)
|
||||
} else {
|
||||
help = append(help, "client_secret was not set in the token request form")
|
||||
}
|
||||
|
||||
if form.RedirectURI != nil {
|
||||
c.Request.Form.Set("redirect_uri", *form.RedirectURI)
|
||||
} else {
|
||||
help = append(help, "redirect_uri was not set in the token request form")
|
||||
}
|
||||
|
||||
var code string
|
||||
if form.Code != nil {
|
||||
if grantType != "authorization_code" {
|
||||
help = append(help, "a code was provided in the token request form, but grant_type was not set to authorization_code")
|
||||
} else {
|
||||
code = *form.Code
|
||||
c.Request.Form.Set("code", code)
|
||||
}
|
||||
} else if grantType == "authorization_code" {
|
||||
help = append(help, "code was not set in the token request form, but must be set since grant_type is authorization_code")
|
||||
}
|
||||
|
||||
if form.Scope != nil {
|
||||
c.Request.Form.Set("scope", *form.Scope)
|
||||
}
|
||||
|
||||
if len(help) != 0 {
|
||||
api.OAuthErrorHandler(c, gtserror.NewErrorBadRequest(oauth.InvalidRequest(), help...))
|
||||
return
|
||||
}
|
||||
|
||||
token, errWithCode := m.processor.OAuthHandleTokenRequest(c.Request)
|
||||
if errWithCode != nil {
|
||||
api.OAuthErrorHandler(c, errWithCode)
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Cache-Control", "no-store")
|
||||
c.Header("Pragma", "no-cache")
|
||||
c.JSON(http.StatusOK, token)
|
||||
}
|
||||
|
|
215
internal/api/client/auth/token_test.go
Normal file
215
internal/api/client/auth/token_test.go
Normal file
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type TokenTestSuite struct {
|
||||
AuthStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *TokenTestSuite) TestPOSTTokenEmptyForm() {
|
||||
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", []byte{}, "")
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
suite.authModule.TokenPOSTHandler(ctx)
|
||||
|
||||
suite.Equal(http.StatusBadRequest, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: grant_type was not set in the token request form, but must be set to authorization_code or client_credentials: client_id was not set in the token request form: client_secret was not set in the token request form: redirect_uri was not set in the token request form"}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *TokenTestSuite) TestRetrieveClientCredentialsOK() {
|
||||
testClient := suite.testClients["local_account_1"]
|
||||
|
||||
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||
"", "",
|
||||
map[string]string{
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": testClient.ID,
|
||||
"client_secret": testClient.Secret,
|
||||
"redirect_uri": "http://localhost:8080",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bodyBytes := requestBody.Bytes()
|
||||
|
||||
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType())
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
suite.authModule.TokenPOSTHandler(ctx)
|
||||
|
||||
suite.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
t := &apimodel.Token{}
|
||||
err = json.Unmarshal(b, t)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal("Bearer", t.TokenType)
|
||||
suite.NotEmpty(t.AccessToken)
|
||||
suite.NotEmpty(t.CreatedAt)
|
||||
suite.WithinDuration(time.Now(), time.Unix(t.CreatedAt, 0), 1*time.Minute)
|
||||
|
||||
// there should be a token in the database now too
|
||||
dbToken := >smodel.Token{}
|
||||
err = suite.db.GetWhere(context.Background(), []db.Where{{Key: "access", Value: t.AccessToken}}, dbToken)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(dbToken)
|
||||
}
|
||||
|
||||
func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeOK() {
|
||||
testClient := suite.testClients["local_account_1"]
|
||||
testUserAuthorizationToken := suite.testTokens["local_account_1_user_authorization_token"]
|
||||
|
||||
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||
"", "",
|
||||
map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": testClient.ID,
|
||||
"client_secret": testClient.Secret,
|
||||
"redirect_uri": "http://localhost:8080",
|
||||
"code": testUserAuthorizationToken.Code,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bodyBytes := requestBody.Bytes()
|
||||
|
||||
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType())
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
suite.authModule.TokenPOSTHandler(ctx)
|
||||
|
||||
suite.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
t := &apimodel.Token{}
|
||||
err = json.Unmarshal(b, t)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal("Bearer", t.TokenType)
|
||||
suite.NotEmpty(t.AccessToken)
|
||||
suite.NotEmpty(t.CreatedAt)
|
||||
suite.WithinDuration(time.Now(), time.Unix(t.CreatedAt, 0), 1*time.Minute)
|
||||
|
||||
dbToken := >smodel.Token{}
|
||||
err = suite.db.GetWhere(context.Background(), []db.Where{{Key: "access", Value: t.AccessToken}}, dbToken)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(dbToken)
|
||||
}
|
||||
|
||||
func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeNoCode() {
|
||||
testClient := suite.testClients["local_account_1"]
|
||||
|
||||
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||
"", "",
|
||||
map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": testClient.ID,
|
||||
"client_secret": testClient.Secret,
|
||||
"redirect_uri": "http://localhost:8080",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bodyBytes := requestBody.Bytes()
|
||||
|
||||
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType())
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
suite.authModule.TokenPOSTHandler(ctx)
|
||||
|
||||
suite.Equal(http.StatusBadRequest, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: code was not set in the token request form, but must be set since grant_type is authorization_code"}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeWrongGrantType() {
|
||||
testClient := suite.testClients["local_account_1"]
|
||||
|
||||
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||
"", "",
|
||||
map[string]string{
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": testClient.ID,
|
||||
"client_secret": testClient.Secret,
|
||||
"redirect_uri": "http://localhost:8080",
|
||||
"code": "peepeepoopoo",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bodyBytes := requestBody.Bytes()
|
||||
|
||||
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType())
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
suite.authModule.TokenPOSTHandler(ctx)
|
||||
|
||||
suite.Equal(http.StatusBadRequest, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: a code was provided in the token request form, but grant_type was not set to authorization_code"}`, string(b))
|
||||
}
|
||||
|
||||
func TestTokenTestSuite(t *testing.T) {
|
||||
suite.Run(t, &TokenTestSuite{})
|
||||
}
|
|
@ -19,13 +19,13 @@
|
|||
package blocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -80,24 +80,25 @@
|
|||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/account"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) BlocksGETHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "PublicTimelineGETHandler")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("error authing: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -118,8 +119,8 @@ func (m *Module) BlocksGETHandler(c *gin.Context) {
|
|||
if limitString != "" {
|
||||
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing limit string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
limit = int(i)
|
||||
|
@ -127,8 +128,7 @@ func (m *Module) BlocksGETHandler(c *gin.Context) {
|
|||
|
||||
resp, errWithCode := m.processor.BlocksGet(c.Request.Context(), authed, maxID, sinceID, limit)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error from processor BlocksGet: %s", errWithCode)
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -5,18 +5,25 @@
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// EmojisGETHandler returns a list of custom emojis enabled on the instance
|
||||
func (m *Module) EmojisGETHandler(c *gin.Context) {
|
||||
if _, err := oauth.Authed(c, true, true, true, true); err != nil {
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
emojis, errWithCode := m.processor.CustomEmojisGet(c)
|
||||
if errWithCode != nil {
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,29 +1,26 @@
|
|||
package favourites
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FavouritesGETHandler handles GETting favourites.
|
||||
func (m *Module) FavouritesGETHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "PublicTimelineGETHandler")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("error authing: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -44,8 +41,8 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) {
|
|||
if limitString != "" {
|
||||
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing limit string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
limit = int(i)
|
||||
|
@ -53,13 +50,12 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) {
|
|||
|
||||
resp, errWithCode := m.processor.FavedTimelineGet(c.Request.Context(), authed, maxID, minID, limit)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error from processor FavedTimelineGet: %s", errWithCode)
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.LinkHeader != "" {
|
||||
c.Header("Link", resp.LinkHeader)
|
||||
}
|
||||
c.JSON(http.StatusOK, resp.Statuses)
|
||||
c.JSON(http.StatusOK, resp.Items)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
package fileserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
|
@ -26,6 +27,7 @@
|
|||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -34,17 +36,9 @@
|
|||
// Note: to mitigate scraping attempts, no information should be given out on a bad request except "404 page not found".
|
||||
// Don't give away account ids or media ids or anything like that; callers shouldn't be able to infer anything.
|
||||
func (m *FileServer) ServeFile(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "ServeFile",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
l.Trace("received request")
|
||||
|
||||
authed, err := oauth.Authed(c, false, false, false, false)
|
||||
if err != nil {
|
||||
c.String(http.StatusNotFound, "404 page not found")
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -53,29 +47,29 @@ func (m *FileServer) ServeFile(c *gin.Context) {
|
|||
// "FILE_NAME" consists of two parts, the attachment's database id, a period, and the file extension.
|
||||
accountID := c.Param(AccountIDKey)
|
||||
if accountID == "" {
|
||||
l.Debug("missing accountID from request")
|
||||
c.String(http.StatusNotFound, "404 page not found")
|
||||
err := fmt.Errorf("missing %s from request", AccountIDKey)
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
mediaType := c.Param(MediaTypeKey)
|
||||
if mediaType == "" {
|
||||
l.Debug("missing mediaType from request")
|
||||
c.String(http.StatusNotFound, "404 page not found")
|
||||
err := fmt.Errorf("missing %s from request", MediaTypeKey)
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
mediaSize := c.Param(MediaSizeKey)
|
||||
if mediaSize == "" {
|
||||
l.Debug("missing mediaSize from request")
|
||||
c.String(http.StatusNotFound, "404 page not found")
|
||||
err := fmt.Errorf("missing %s from request", MediaSizeKey)
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := c.Param(FileNameKey)
|
||||
if fileName == "" {
|
||||
l.Debug("missing fileName from request")
|
||||
c.String(http.StatusNotFound, "404 page not found")
|
||||
err := fmt.Errorf("missing %s from request", FileNameKey)
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -86,8 +80,7 @@ func (m *FileServer) ServeFile(c *gin.Context) {
|
|||
FileName: fileName,
|
||||
})
|
||||
if errWithCode != nil {
|
||||
l.Errorf(errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -95,7 +88,7 @@ func (m *FileServer) ServeFile(c *gin.Context) {
|
|||
// if the content is a ReadCloser, close it when we're done
|
||||
if closer, ok := content.Content.(io.ReadCloser); ok {
|
||||
if err := closer.Close(); err != nil {
|
||||
l.Errorf("error closing readcloser: %s", err)
|
||||
logrus.Errorf("ServeFile: error closing readcloser: %s", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
@ -103,9 +96,9 @@ func (m *FileServer) ServeFile(c *gin.Context) {
|
|||
// TODO: if the requester only accepts text/html we should try to serve them *something*.
|
||||
// This is mostly needed because when sharing a link to a gts-hosted file on something like mastodon, the masto servers will
|
||||
// attempt to look up the content to provide a preview of the link, and they ask for text/html.
|
||||
format, err := api.NegotiateAccept(c, api.Offer(content.ContentType))
|
||||
if errWithCode != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
format, err := api.NegotiateAccept(c, api.MIME(content.ContentType))
|
||||
if err != nil {
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -5,12 +5,19 @@
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FiltersGETHandler returns a list of filters set by/for the authed account
|
||||
func (m *Module) FiltersGETHandler(c *gin.Context) {
|
||||
if _, err := oauth.Authed(c, true, true, true, true); err != nil {
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,12 +19,12 @@
|
|||
package followrequest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -62,43 +62,34 @@
|
|||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FollowRequestAuthorizePOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "FollowRequestAuthorizePOSTHandler")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
originAccountID := c.Param(IDKey)
|
||||
if originAccountID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"})
|
||||
err := errors.New("no account id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
relationship, errWithCode := m.processor.FollowRequestAccept(c.Request.Context(), authed, originAccountID)
|
||||
if errWithCode != nil {
|
||||
l.Debug(errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -82,6 +82,34 @@ func (suite *AuthorizeTestSuite) TestAuthorize() {
|
|||
suite.Equal(`{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","following":false,"showing_reblogs":false,"notifying":false,"followed_by":true,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false,"note":""}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *AuthorizeTestSuite) TestAuthorizeNoFR() {
|
||||
requestingAccount := suite.testAccounts["remote_account_2"]
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodPost, []byte{}, fmt.Sprintf("/api/v1/follow_requests/%s/authorize", requestingAccount.ID), "")
|
||||
|
||||
ctx.Params = gin.Params{
|
||||
gin.Param{
|
||||
Key: followrequest.IDKey,
|
||||
Value: requestingAccount.ID,
|
||||
},
|
||||
}
|
||||
|
||||
// call the handler
|
||||
suite.followRequestModule.FollowRequestAuthorizePOSTHandler(ctx)
|
||||
|
||||
suite.Equal(http.StatusNotFound, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
// check the response
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
suite.Equal(`{"error":"Not Found"}`, string(b))
|
||||
}
|
||||
|
||||
func TestAuthorizeTestSuite(t *testing.T) {
|
||||
suite.Run(t, &AuthorizeTestSuite{})
|
||||
}
|
||||
|
|
|
@ -21,10 +21,9 @@
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -71,34 +70,27 @@
|
|||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FollowRequestGETHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "FollowRequestGETHandler")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
accts, errWithCode := m.processor.FollowRequestsGet(c.Request.Context(), authed)
|
||||
if errWithCode != nil {
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ func (suite *GetTestSuite) TestGet() {
|
|||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
suite.Equal(`[{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28.00Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]}]`, string(b))
|
||||
suite.Equal(`[{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28.000Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]}]`, string(b))
|
||||
}
|
||||
|
||||
func TestGetTestSuite(t *testing.T) {
|
||||
|
|
|
@ -19,11 +19,12 @@
|
|||
package followrequest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -59,43 +60,34 @@
|
|||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FollowRequestRejectPOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "FollowRequestRejectPOSTHandler")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
originAccountID := c.Param(IDKey)
|
||||
if originAccountID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"})
|
||||
err := errors.New("no account id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
relationship, errWithCode := m.processor.FollowRequestReject(c.Request.Context(), authed, originAccountID)
|
||||
if errWithCode != nil {
|
||||
l.Debug(errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
@ -30,22 +30,19 @@
|
|||
// description: "Instance information."
|
||||
// schema:
|
||||
// "$ref": "#/definitions/instance"
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal error
|
||||
func (m *Module) InstanceInformationGETHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "InstanceInformationGETHandler")
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
host := config.GetHost()
|
||||
|
||||
instance, err := m.processor.InstanceGet(c.Request.Context(), host)
|
||||
if err != nil {
|
||||
l.Debugf("error getting instance from processor: %s", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
||||
instance, errWithCode := m.processor.InstanceGet(c.Request.Context(), config.GetHost())
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package instance
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -82,52 +82,51 @@
|
|||
// description: "The newly updated instance."
|
||||
// schema:
|
||||
// "$ref": "#/definitions/instance"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "InstanceUpdatePATCHHandler")
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// only admins can update instance settings
|
||||
if !authed.User.Admin {
|
||||
l.Debug("user is not an admin so cannot update instance settings")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "not an admin"})
|
||||
err := errors.New("user is not an admin so cannot update instance settings")
|
||||
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
l.Debug("parsing request form")
|
||||
form := &model.InstanceSettingsUpdateRequest{}
|
||||
if err := c.ShouldBind(&form); err != nil || form == nil {
|
||||
l.Debugf("could not parse form from request: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
if err := c.ShouldBind(&form); err != nil {
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
l.Debugf("parsed form: %+v", form)
|
||||
|
||||
// if everything on the form is nil, then nothing has been set and we shouldn't continue
|
||||
if form.Title == nil && form.ContactUsername == nil && form.ContactEmail == nil && form.ShortDescription == nil && form.Description == nil && form.Terms == nil && form.Avatar == nil && form.Header == nil {
|
||||
l.Debugf("could not parse form from request")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"})
|
||||
err := errors.New("empty form submitted")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
i, errWithCode := m.processor.InstancePatch(c.Request.Context(), form)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error with instance patch request: %s", errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/instance"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
|
@ -62,7 +63,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
|||
b, err := io.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{"uri":"http://localhost:8080","title":"Example Instance","description":"","short_description":"","email":"someone@example.org","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.00Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.00Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b))
|
||||
suite.Equal(`{"uri":"http://localhost:8080","title":"Example Instance","description":"","short_description":"","email":"someone@example.org","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
||||
|
@ -125,6 +126,67 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
|||
suite.Equal(`{"uri":"http://localhost:8080","title":"localhost:8080","description":"","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *InstancePatchTestSuite) TestInstancePatch4() {
|
||||
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||
"", "",
|
||||
map[string]string{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bodyBytes := requestBody.Bytes()
|
||||
|
||||
// set up the request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, instance.InstanceInformationPath, w.FormDataContentType())
|
||||
|
||||
// call the handler
|
||||
suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
|
||||
|
||||
suite.Equal(http.StatusBadRequest, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{"error":"Bad Request: empty form submitted"}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *InstancePatchTestSuite) TestInstancePatch5() {
|
||||
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||
"", "",
|
||||
map[string]string{
|
||||
"short_description": "<p>This is some html, which is <em>allowed</em> in short descriptions.</p>",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bodyBytes := requestBody.Bytes()
|
||||
|
||||
// set up the request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, instance.InstanceInformationPath, w.FormDataContentType())
|
||||
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// call the handler
|
||||
suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
|
||||
|
||||
suite.Equal(http.StatusForbidden, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{"error":"Forbidden: user is not an admin so cannot update instance settings"}`, string(b))
|
||||
}
|
||||
|
||||
func TestInstancePatchTestSuite(t *testing.T) {
|
||||
suite.Run(t, &InstancePatchTestSuite{})
|
||||
}
|
||||
|
|
|
@ -5,12 +5,19 @@
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// ListsGETHandler returns a list of lists created by/for the authed account
|
||||
func (m *Module) ListsGETHandler(c *gin.Context) {
|
||||
if _, err := oauth.Authed(c, true, true, true, true); err != nil {
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -23,12 +23,11 @@
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -80,46 +79,36 @@
|
|||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '422':
|
||||
// description: unprocessable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) MediaCreatePOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "statusCreatePOSTHandler")
|
||||
authed, err := oauth.Authed(c, true, true, true, true) // posting new media is serious business so we want *everything*
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// extract the media create form from the request context
|
||||
l.Tracef("parsing request form: %s", c.Request.Form)
|
||||
form := &model.AttachmentRequest{}
|
||||
if err := c.ShouldBind(&form); err != nil {
|
||||
l.Debugf("error parsing form: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("could not parse form: %s", err)})
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// Give the fields on the request form a first pass to make sure the request is superficially valid.
|
||||
l.Tracef("validating form %+v", form)
|
||||
if err := validateCreateMedia(form); err != nil {
|
||||
l.Debugf("error validating form: %s", err)
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
l.Debug("calling processor media create func")
|
||||
apiAttachment, err := m.processor.MediaCreate(c.Request.Context(), authed, form)
|
||||
apiAttachment, errWithCode := m.processor.MediaCreate(c.Request.Context(), authed, form)
|
||||
if err != nil {
|
||||
l.Debugf("error creating attachment: %s", err)
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -143,6 +132,7 @@ func validateCreateMedia(form *model.AttachmentRequest) error {
|
|||
if maxImageSize > maxSize {
|
||||
maxSize = maxImageSize
|
||||
}
|
||||
|
||||
if form.File.Size > int64(maxSize) {
|
||||
return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size)
|
||||
}
|
||||
|
|
|
@ -247,15 +247,14 @@ func (suite *MediaCreateTestSuite) TestMediaCreateLongDescription() {
|
|||
suite.mediaModule.MediaCreatePOSTHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusUnprocessableEntity, recorder.Code)
|
||||
suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
expectedErr := fmt.Sprintf(`{"error":"image description length must be between 0 and 500 characters (inclusive), but provided image description was %d chars"}`, len(description))
|
||||
suite.Equal(expectedErr, string(b))
|
||||
suite.Equal(`{"error":"Bad Request: image description length must be between 0 and 500 characters (inclusive), but provided image description was 6667 chars"}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *MediaCreateTestSuite) TestMediaCreateTooShortDescription() {
|
||||
|
|
|
@ -19,12 +19,12 @@
|
|||
package media
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -59,33 +59,34 @@
|
|||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '422':
|
||||
// description: unprocessable
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) MediaGETHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "MediaGETHandler")
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
attachmentID := c.Param(IDKey)
|
||||
if attachmentID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"})
|
||||
err := errors.New("no attachment id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
attachment, errWithCode := m.processor.MediaGet(c.Request.Context(), authed, attachmentID)
|
||||
if errWithCode != nil {
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -23,12 +23,11 @@
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -89,50 +88,45 @@
|
|||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '422':
|
||||
// description: unprocessable
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) MediaPUTHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "MediaGETHandler")
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
attachmentID := c.Param(IDKey)
|
||||
if attachmentID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"})
|
||||
err := errors.New("no attachment id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// extract the media update form from the request context
|
||||
l.Tracef("parsing request form: %s", c.Request.Form)
|
||||
var form model.AttachmentUpdateRequest
|
||||
if err := c.ShouldBind(&form); err != nil {
|
||||
l.Debugf("could not parse form from request: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
|
||||
form := &model.AttachmentUpdateRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// Give the fields on the request form a first pass to make sure the request is superficially valid.
|
||||
l.Tracef("validating form %+v", form)
|
||||
if err := validateUpdateMedia(&form); err != nil {
|
||||
l.Debugf("error validating form: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
if err := validateUpdateMedia(form); err != nil {
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
attachment, errWithCode := m.processor.MediaUpdate(c.Request.Context(), authed, attachmentID, &form)
|
||||
attachment, errWithCode := m.processor.MediaUpdate(c.Request.Context(), authed, attachmentID, form)
|
||||
if errWithCode != nil {
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -232,7 +232,7 @@ func (suite *MediaUpdateTestSuite) TestUpdateImageShortDescription() {
|
|||
suite.NoError(err)
|
||||
|
||||
// reply should be an error message
|
||||
suite.Equal(`{"error":"image description length must be between 50 and 500 characters (inclusive), but provided image description was 16 chars"}`, string(b))
|
||||
suite.Equal(`{"error":"Bad Request: image description length must be between 50 and 500 characters (inclusive), but provided image description was 16 chars"}`, string(b))
|
||||
}
|
||||
|
||||
func TestMediaUpdateTestSuite(t *testing.T) {
|
||||
|
|
|
@ -19,34 +19,26 @@
|
|||
package notification
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// NotificationsGETHandler serves a list of notifications to the caller, with the desired query parameters
|
||||
func (m *Module) NotificationsGETHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "NotificationsGETHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
l.Debugf("entering function")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Errorf("error authing status faved by request: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -55,8 +47,8 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
|
|||
if limitString != "" {
|
||||
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing limit string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
limit = int(i)
|
||||
|
@ -74,12 +66,14 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
|
|||
sinceID = sinceIDString
|
||||
}
|
||||
|
||||
notifs, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, limit, maxID, sinceID)
|
||||
resp, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, limit, maxID, sinceID)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error processing notifications get: %s", errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, notifs)
|
||||
if resp.LinkHeader != "" {
|
||||
c.Header("Link", resp.LinkHeader)
|
||||
}
|
||||
c.JSON(http.StatusOK, resp.Items)
|
||||
}
|
||||
|
|
|
@ -19,14 +19,15 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -52,50 +53,44 @@
|
|||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/searchResult"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) SearchGETHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "SearchGETHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
l.Debugf("entering function")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Errorf("error authing search request: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
accountID := c.Query(AccountIDKey)
|
||||
maxID := c.Query(MaxIDKey)
|
||||
minID := c.Query(MinIDKey)
|
||||
searchType := c.Query(TypeKey)
|
||||
|
||||
excludeUnreviewed := false
|
||||
excludeUnreviewedString := c.Query(ExcludeUnreviewedKey)
|
||||
if excludeUnreviewedString != "" {
|
||||
var err error
|
||||
excludeUnreviewed, err = strconv.ParseBool(excludeUnreviewedString)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", excludeUnreviewedString, err)})
|
||||
err := fmt.Errorf("error parsing %s: %s", ExcludeUnreviewedKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
query := c.Query(QueryKey)
|
||||
if query == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter q was empty"})
|
||||
err := errors.New("query parameter q was empty")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -105,18 +100,19 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
|
|||
var err error
|
||||
resolve, err = strconv.ParseBool(resolveString)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", resolveString, err)})
|
||||
err := fmt.Errorf("error parsing %s: %s", ResolveKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
limit := 20
|
||||
limit := 2
|
||||
limitString := c.Query(LimitKey)
|
||||
if limitString != "" {
|
||||
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing limit string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
limit = int(i)
|
||||
|
@ -133,18 +129,12 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
|
|||
if offsetString != "" {
|
||||
i, err := strconv.ParseInt(offsetString, 10, 64)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing offset string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse offset query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", OffsetKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
offset = int(i)
|
||||
}
|
||||
if limit > 40 {
|
||||
limit = 40
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 1
|
||||
}
|
||||
|
||||
following := false
|
||||
followingString := c.Query(FollowingKey)
|
||||
|
@ -152,16 +142,17 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
|
|||
var err error
|
||||
following, err = strconv.ParseBool(followingString)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", followingString, err)})
|
||||
err := fmt.Errorf("error parsing %s: %s", FollowingKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
searchQuery := &model.SearchQuery{
|
||||
AccountID: accountID,
|
||||
MaxID: maxID,
|
||||
MinID: minID,
|
||||
Type: searchType,
|
||||
AccountID: c.Query(AccountIDKey),
|
||||
MaxID: c.Query(MaxIDKey),
|
||||
MinID: c.Query(MinIDKey),
|
||||
Type: c.Query(TypeKey),
|
||||
ExcludeUnreviewed: excludeUnreviewed,
|
||||
Query: query,
|
||||
Resolve: resolve,
|
||||
|
@ -172,8 +163,7 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
|
|||
|
||||
results, errWithCode := m.processor.SearchGet(c.Request.Context(), authed, searchQuery)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error searching: %s", errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,11 +19,12 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -66,37 +67,32 @@
|
|||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) StatusBoostPOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "StatusBoostPOSTHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
l.Debugf("entering function")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debug("not authed so can't boost status")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetStatusID := c.Param(IDKey)
|
||||
if targetStatusID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
||||
err := errors.New("no status id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
apiStatus, errWithCode := m.processor.StatusBoost(c.Request.Context(), authed, targetStatusID)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error processing status boost: %s", errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -134,13 +134,13 @@ func (suite *StatusBoostTestSuite) TestPostUnboostable() {
|
|||
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusForbidden, recorder.Code) // we 403 unboostable statuses
|
||||
suite.Equal(http.StatusForbidden, recorder.Code) // we 403 unboostable statuses
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), `{"error":"forbidden"}`, string(b))
|
||||
assert.Equal(suite.T(), `{"error":"Forbidden"}`, string(b))
|
||||
}
|
||||
|
||||
// try to boost a status that's not visible to the user
|
||||
|
@ -177,13 +177,7 @@ func (suite *StatusBoostTestSuite) TestPostNotVisible() {
|
|||
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), `{"error":"404 not found"}`, string(b))
|
||||
suite.Equal(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible
|
||||
}
|
||||
|
||||
func TestStatusBoostTestSuite(t *testing.T) {
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -84,10 +85,9 @@ func (m *Module) StatusBoostedByGETHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
apiAccounts, err := m.processor.StatusBoostedBy(c.Request.Context(), authed, targetStatusID)
|
||||
if err != nil {
|
||||
l.Debugf("error processing status boosted by request: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
||||
apiAccounts, errWithCode := m.processor.StatusBoostedBy(c.Request.Context(), authed, targetStatusID)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,11 +19,12 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -65,37 +66,32 @@
|
|||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) StatusContextGETHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "StatusContextGETHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
l.Debugf("entering function")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Errorf("error authing status context request: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetStatusID := c.Param(IDKey)
|
||||
if targetStatusID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
||||
err := errors.New("no status id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
statusContext, errWithCode := m.processor.StatusGetContext(c.Request.Context(), authed, targetStatusID)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error getting status context: %s", errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -23,12 +23,11 @@
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
)
|
||||
|
@ -61,58 +60,44 @@
|
|||
// description: "The newly created status."
|
||||
// schema:
|
||||
// "$ref": "#/definitions/status"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal error
|
||||
// description: internal server error
|
||||
func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "statusCreatePOSTHandler")
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// First check this user/account is permitted to post new statuses.
|
||||
// There's no point continuing otherwise.
|
||||
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
|
||||
return
|
||||
}
|
||||
|
||||
// extract the status create form from the request context
|
||||
l.Debugf("parsing request form: %s", c.Request.Form)
|
||||
form := &model.AdvancedStatusCreateForm{}
|
||||
if err := c.ShouldBind(form); err != nil || form == nil {
|
||||
l.Debugf("could not parse form from request: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
l.Debugf("handling status request form: %+v", form)
|
||||
|
||||
// Give the fields on the request form a first pass to make sure the request is superficially valid.
|
||||
l.Tracef("validating form %+v", form)
|
||||
if err := validateCreateStatus(form); err != nil {
|
||||
l.Debugf("error validating form: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
apiStatus, err := m.processor.StatusCreate(c.Request.Context(), authed, form)
|
||||
if err != nil {
|
||||
l.Debugf("error processing status create: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
||||
apiStatus, errWithCode := m.processor.StatusCreate(c.Request.Context(), authed, form)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -120,7 +105,6 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
|
|||
}
|
||||
|
||||
func validateCreateStatus(form *model.AdvancedStatusCreateForm) error {
|
||||
// validate that, structurally, we have a valid status/post
|
||||
if form.Status == "" && form.MediaIDs == nil && form.Poll == nil {
|
||||
return errors.New("no status, media, or poll provided")
|
||||
}
|
||||
|
@ -135,19 +119,16 @@ func validateCreateStatus(form *model.AdvancedStatusCreateForm) error {
|
|||
maxPollChars := config.GetStatusesPollOptionMaxChars()
|
||||
maxCwChars := config.GetStatusesCWMaxChars()
|
||||
|
||||
// validate status
|
||||
if form.Status != "" {
|
||||
if len(form.Status) > maxChars {
|
||||
return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), maxChars)
|
||||
}
|
||||
}
|
||||
|
||||
// validate media attachments
|
||||
if len(form.MediaIDs) > maxMediaFiles {
|
||||
return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), maxMediaFiles)
|
||||
}
|
||||
|
||||
// validate poll
|
||||
if form.Poll != nil {
|
||||
if form.Poll.Options == nil {
|
||||
return errors.New("poll with no options")
|
||||
|
@ -162,14 +143,12 @@ func validateCreateStatus(form *model.AdvancedStatusCreateForm) error {
|
|||
}
|
||||
}
|
||||
|
||||
// validate spoiler text/cw
|
||||
if form.SpoilerText != "" {
|
||||
if len(form.SpoilerText) > maxCwChars {
|
||||
return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), maxCwChars)
|
||||
}
|
||||
}
|
||||
|
||||
// validate post language
|
||||
if form.Language != "" {
|
||||
if err := validate.Language(form.Language); err != nil {
|
||||
return err
|
||||
|
|
|
@ -256,7 +256,7 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
|
|||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
suite.Equal(`{"error":"bad request"}`, string(b))
|
||||
suite.Equal(`{"error":"Bad Request: status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b))
|
||||
}
|
||||
|
||||
// Post a reply to the status of a local user that allows replies.
|
||||
|
|
|
@ -19,11 +19,12 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -65,43 +66,32 @@
|
|||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) StatusDELETEHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "StatusDELETEHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
l.Debugf("entering function")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debug("not authed so can't delete status")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetStatusID := c.Param(IDKey)
|
||||
if targetStatusID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
||||
err := errors.New("no status id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
apiStatus, err := m.processor.StatusDelete(c.Request.Context(), authed, targetStatusID)
|
||||
if err != nil {
|
||||
l.Debugf("error processing status delete: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
||||
return
|
||||
}
|
||||
|
||||
// the status was already gone/never existed
|
||||
if apiStatus == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Record not found"})
|
||||
apiStatus, errWithCode := m.processor.StatusDelete(c.Request.Context(), authed, targetStatusID)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,11 +19,12 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -62,37 +63,32 @@
|
|||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) StatusFavePOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "StatusFavePOSTHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
l.Debugf("entering function")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debug("not authed so can't fave status")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetStatusID := c.Param(IDKey)
|
||||
if targetStatusID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
||||
err := errors.New("no status id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
apiStatus, err := m.processor.StatusFave(c.Request.Context(), authed, targetStatusID)
|
||||
if err != nil {
|
||||
l.Debugf("error processing status fave: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
||||
apiStatus, errWithCode := m.processor.StatusFave(c.Request.Context(), authed, targetStatusID)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -118,13 +118,13 @@ func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
|
|||
suite.statusModule.StatusFavePOSTHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
||||
suite.EqualValues(http.StatusForbidden, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), `{"error":"bad request"}`, string(b))
|
||||
assert.Equal(suite.T(), `{"error":"Forbidden"}`, string(b))
|
||||
}
|
||||
|
||||
func TestStatusFaveTestSuite(t *testing.T) {
|
||||
|
|
|
@ -19,11 +19,12 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -63,37 +64,32 @@
|
|||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) StatusFavedByGETHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "statusGETHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
l.Debugf("entering function")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Errorf("error authing status faved by request: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetStatusID := c.Param(IDKey)
|
||||
if targetStatusID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
||||
err := errors.New("no status id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
apiAccounts, err := m.processor.StatusFavedBy(c.Request.Context(), authed, targetStatusID)
|
||||
if err != nil {
|
||||
l.Debugf("error processing status faved by request: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
||||
apiAccounts, errWithCode := m.processor.StatusFavedBy(c.Request.Context(), authed, targetStatusID)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,11 +19,12 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -54,45 +55,40 @@
|
|||
// description: "The requested created status."
|
||||
// schema:
|
||||
// "$ref": "#/definitions/status"
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal error
|
||||
// description: internal server error
|
||||
func (m *Module) StatusGETHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "statusGETHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
l.Debugf("entering function")
|
||||
|
||||
authed, err := oauth.Authed(c, false, false, false, false)
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Errorf("error authing status faved by request: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetStatusID := c.Param(IDKey)
|
||||
if targetStatusID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
||||
err := errors.New("no status id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
apiStatus, err := m.processor.StatusGet(c.Request.Context(), authed, targetStatusID)
|
||||
if err != nil {
|
||||
l.Debugf("error processing status get: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
||||
apiStatus, errWithCode := m.processor.StatusGet(c.Request.Context(), authed, targetStatusID)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,11 +19,12 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -63,37 +64,32 @@
|
|||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) StatusUnboostPOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "StatusUnboostPOSTHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
l.Debugf("entering function")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debug("not authed so can't unboost status")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetStatusID := c.Param(IDKey)
|
||||
if targetStatusID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
||||
err := errors.New("no status id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
apiStatus, errWithCode := m.processor.StatusUnboost(c.Request.Context(), authed, targetStatusID)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error processing status unboost: %s", errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,11 +19,12 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -62,37 +63,32 @@
|
|||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "StatusUnfavePOSTHandler",
|
||||
"request_uri": c.Request.RequestURI,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
"origin_ip": c.ClientIP(),
|
||||
})
|
||||
l.Debugf("entering function")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debug("not authed so can't unfave status")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
targetStatusID := c.Param(IDKey)
|
||||
if targetStatusID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
||||
err := errors.New("no status id specified")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
apiStatus, err := m.processor.StatusUnfave(c.Request.Context(), authed, targetStatusID)
|
||||
if err != nil {
|
||||
l.Debugf("error processing status unfave: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
||||
apiStatus, errWithCode := m.processor.StatusUnfave(c.Request.Context(), authed, targetStatusID)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,24 @@
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var wsUpgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
// we expect cors requests (via eg., pinafore.social) so be lenient
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
// StreamGETHandler swagger:operation GET /api/v1/streaming streamGet
|
||||
//
|
||||
// Initiate a websocket connection for live streaming of statuses and notifications.
|
||||
|
@ -108,79 +118,78 @@
|
|||
// '400':
|
||||
// description: bad request
|
||||
func (m *Module) StreamGETHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "StreamGETHandler")
|
||||
|
||||
streamType := c.Query(StreamQueryKey)
|
||||
if streamType == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("no stream type provided under query key %s", StreamQueryKey)})
|
||||
err := fmt.Errorf("no stream type provided under query key %s", StreamQueryKey)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
accessToken := c.Query(AccessTokenQueryKey)
|
||||
if accessToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("no access token provided under query key %s", AccessTokenQueryKey)})
|
||||
err := fmt.Errorf("no access token provided under query key %s", AccessTokenQueryKey)
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// make sure a valid token has been provided and obtain the associated account
|
||||
account, err := m.processor.AuthorizeStreamingRequest(c.Request.Context(), accessToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "could not authorize with given token"})
|
||||
return
|
||||
}
|
||||
|
||||
// prepare to upgrade the connection to a websocket connection
|
||||
upgrader := websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// we fully expect cors requests (via something like pinafore.social) so we should be lenient here
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// do the actual upgrade here
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
l.Infof("error upgrading websocket connection: %s", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close() // whatever happens, when we leave this function we want to close the websocket connection
|
||||
|
||||
// inform the processor that we have a new connection and want a s for it
|
||||
s, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType)
|
||||
account, errWithCode := m.processor.AuthorizeStreamingRequest(c.Request.Context(), accessToken)
|
||||
if errWithCode != nil {
|
||||
c.JSON(errWithCode.Code(), errWithCode.Safe())
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
defer close(s.Hangup) // closing stream.Hangup indicates that we've finished with the connection (the client has gone), so we want to do this on exiting this handler
|
||||
|
||||
// spawn a new ticker for pinging the connection periodically
|
||||
t := time.NewTicker(30 * time.Second)
|
||||
stream, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// we want to stay in the sendloop as long as possible while the client is connected -- the only thing that should break the loop is if the client leaves or something else goes wrong
|
||||
sendLoop:
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"account": account.Username,
|
||||
"path": BasePath,
|
||||
"streamID": stream.ID,
|
||||
"streamType": streamType,
|
||||
})
|
||||
|
||||
wsConn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
// If the upgrade fails, then Upgrade replies to the client with an HTTP error response.
|
||||
// Because websocket issues are a pretty common source of headaches, we should also log
|
||||
// this at Error to make this plenty visible and help admins out a bit.
|
||||
l.Errorf("error upgrading websocket connection: %s", err)
|
||||
close(stream.Hangup)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// cleanup
|
||||
wsConn.Close()
|
||||
close(stream.Hangup)
|
||||
}()
|
||||
|
||||
streamTicker := time.NewTicker(30 * time.Second)
|
||||
|
||||
// We want to stay in the loop as long as possible while the client is connected.
|
||||
// The only thing that should break the loop is if the client leaves or the connection becomes unhealthy.
|
||||
//
|
||||
// If the loop does break, we expect the client to reattempt connection, so it's cheap to leave + try again
|
||||
wsLoop:
|
||||
for {
|
||||
select {
|
||||
case m := <-s.Messages:
|
||||
// we've got a streaming message!!
|
||||
case m := <-stream.Messages:
|
||||
l.Trace("received message from stream")
|
||||
if err := conn.WriteJSON(m); err != nil {
|
||||
l.Debugf("error writing json to websocket connection: %s", err)
|
||||
// if something is wrong we want to bail and drop the connection -- the client will create a new one
|
||||
break sendLoop
|
||||
if err := wsConn.WriteJSON(m); err != nil {
|
||||
l.Debugf("error writing json to websocket connection; breaking off: %s", err)
|
||||
break wsLoop
|
||||
}
|
||||
l.Trace("wrote message into websocket connection")
|
||||
case <-t.C:
|
||||
case <-streamTicker.C:
|
||||
l.Trace("received TICK from ticker")
|
||||
if err := conn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil {
|
||||
l.Debugf("error writing ping to websocket connection: %s", err)
|
||||
// if something is wrong we want to bail and drop the connection -- the client will create a new one
|
||||
break sendLoop
|
||||
if err := wsConn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil {
|
||||
l.Debugf("error writing ping to websocket connection; breaking off: %s", err)
|
||||
break wsLoop
|
||||
}
|
||||
l.Trace("wrote ping message into websocket connection")
|
||||
}
|
||||
}
|
||||
|
||||
l.Trace("leaving StreamGETHandler")
|
||||
}
|
||||
|
|
|
@ -19,13 +19,13 @@
|
|||
package timeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -105,17 +105,14 @@
|
|||
// '400':
|
||||
// description: bad request
|
||||
func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "HomeTimelineGETHandler")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("error authing: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -142,8 +139,8 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
|
|||
if limitString != "" {
|
||||
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing limit string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
limit = int(i)
|
||||
|
@ -154,8 +151,8 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
|
|||
if localString != "" {
|
||||
i, err := strconv.ParseBool(localString)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing local string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse local query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", LocalKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
local = i
|
||||
|
@ -163,13 +160,12 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
|
|||
|
||||
resp, errWithCode := m.processor.HomeTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error from processor HomeTimelineGet: %s", errWithCode)
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.LinkHeader != "" {
|
||||
c.Header("Link", resp.LinkHeader)
|
||||
}
|
||||
c.JSON(http.StatusOK, resp.Statuses)
|
||||
c.JSON(http.StatusOK, resp.Items)
|
||||
}
|
||||
|
|
|
@ -19,13 +19,13 @@
|
|||
package timeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -105,17 +105,14 @@
|
|||
// '400':
|
||||
// description: bad request
|
||||
func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "PublicTimelineGETHandler")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("error authing: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -142,8 +139,8 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
|
|||
if limitString != "" {
|
||||
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing limit string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
limit = int(i)
|
||||
|
@ -154,8 +151,8 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
|
|||
if localString != "" {
|
||||
i, err := strconv.ParseBool(localString)
|
||||
if err != nil {
|
||||
l.Debugf("error parsing local string: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse local query param"})
|
||||
err := fmt.Errorf("error parsing %s: %s", LocalKey, err)
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
local = i
|
||||
|
@ -163,13 +160,12 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
|
|||
|
||||
resp, errWithCode := m.processor.PublicTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local)
|
||||
if errWithCode != nil {
|
||||
l.Debugf("error from processor PublicTimelineGet: %s", errWithCode)
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.LinkHeader != "" {
|
||||
c.Header("Link", resp.LinkHeader)
|
||||
}
|
||||
c.JSON(http.StatusOK, resp.Statuses)
|
||||
c.JSON(http.StatusOK, resp.Items)
|
||||
}
|
||||
|
|
|
@ -19,12 +19,13 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
|
@ -54,48 +55,48 @@
|
|||
// responses:
|
||||
// '200':
|
||||
// description: Change successful
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: "internal error"
|
||||
// description: internal error
|
||||
func (m *Module) PasswordChangePOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "PasswordChangePOSTHandler")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("error authing: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// First check this user/account is active.
|
||||
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
form := &model.PasswordChangeRequest{}
|
||||
if err := c.ShouldBind(form); err != nil || form == nil || form.NewPassword == "" || form.OldPassword == "" {
|
||||
if err != nil {
|
||||
l.Debugf("could not parse form from request: %s", err)
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if form.OldPassword == "" {
|
||||
err := errors.New("password change request missing field old_password")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if form.NewPassword == "" {
|
||||
err := errors.New("password change request missing field new_password")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if errWithCode := m.processor.UserChangePassword(c.Request.Context(), authed, form); errWithCode != nil {
|
||||
l.Debugf("error changing user password: %s", errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordChangePOST() {
|
|||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
ctx.Request.Form = url.Values{
|
||||
"old_password": {"password"},
|
||||
|
@ -83,7 +83,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() {
|
|||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
ctx.Request.Form = url.Values{
|
||||
"new_password": {"peepeepoopoopassword"},
|
||||
|
@ -97,7 +97,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() {
|
|||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
suite.Equal(`{"error":"missing one or more required form values"}`, string(b))
|
||||
suite.Equal(`{"error":"Bad Request: password change request missing field old_password"}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
|
||||
|
@ -110,7 +110,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
|
|||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
ctx.Request.Form = url.Values{
|
||||
"old_password": {"notright"},
|
||||
|
@ -125,7 +125,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
|
|||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
suite.Equal(`{"error":"bad request: old password did not match"}`, string(b))
|
||||
suite.Equal(`{"error":"Bad Request: old password did not match"}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
|
||||
|
@ -138,7 +138,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
|
|||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
ctx.Request.Form = url.Values{
|
||||
"old_password": {"password"},
|
||||
|
@ -153,7 +153,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
|
|||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
suite.Equal(`{"error":"bad request: password is 94% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b))
|
||||
suite.Equal(`{"error":"Bad Request: password is 94% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b))
|
||||
}
|
||||
|
||||
func TestPasswordChangeTestSuite(t *testing.T) {
|
||||
|
|
|
@ -56,8 +56,8 @@ type UserStandardTestSuite struct {
|
|||
}
|
||||
|
||||
func (suite *UserStandardTestSuite) SetupTest() {
|
||||
testrig.InitTestLog()
|
||||
testrig.InitTestConfig()
|
||||
testrig.InitTestLog()
|
||||
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||
suite.testTokens = testrig.NewTestTokens()
|
||||
|
|
154
internal/api/errorhandling.go
Normal file
154
internal/api/errorhandling.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// TODO: add more templated html pages here for different error types
|
||||
|
||||
// NotFoundHandler serves a 404 html page through the provided gin context,
|
||||
// if accept is 'text/html', or just returns a json error if 'accept' is empty
|
||||
// or application/json.
|
||||
//
|
||||
// When serving html, NotFoundHandler calls the provided InstanceGet function
|
||||
// to fetch the apimodel representation of the instance, for serving in the
|
||||
// 404 header and footer.
|
||||
//
|
||||
// If an error is returned by InstanceGet, the function will panic.
|
||||
func NotFoundHandler(c *gin.Context, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode), accept string) {
|
||||
switch accept {
|
||||
case string(TextHTML):
|
||||
host := config.GetHost()
|
||||
instance, err := instanceGet(c.Request.Context(), host)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c.HTML(http.StatusNotFound, "404.tmpl", gin.H{
|
||||
"instance": instance,
|
||||
})
|
||||
default:
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": http.StatusText(http.StatusNotFound)})
|
||||
}
|
||||
}
|
||||
|
||||
// genericErrorHandler is a more general version of the NotFoundHandler, which can
|
||||
// be used for serving either generic error pages with some rendered help text,
|
||||
// or just some error json if the caller prefers (or has no preference).
|
||||
func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode), accept string, errWithCode gtserror.WithCode) {
|
||||
switch accept {
|
||||
case string(TextHTML):
|
||||
host := config.GetHost()
|
||||
instance, err := instanceGet(c.Request.Context(), host)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c.HTML(errWithCode.Code(), "error.tmpl", gin.H{
|
||||
"instance": instance,
|
||||
"code": errWithCode.Code(),
|
||||
"error": errWithCode.Safe(),
|
||||
})
|
||||
default:
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorHandler takes the provided gin context and errWithCode and tries to serve
|
||||
// a helpful error to the caller. It will do content negotiation to figure out if
|
||||
// the caller prefers to see an html page with the error rendered there. If not, or
|
||||
// if something goes wrong during the function, it will recover and just try to serve
|
||||
// an appropriate application/json content-type error.
|
||||
func ErrorHandler(c *gin.Context, errWithCode gtserror.WithCode, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode)) {
|
||||
path := c.Request.URL.Path
|
||||
if raw := c.Request.URL.RawQuery; raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"path": path,
|
||||
"error": errWithCode.Error(),
|
||||
})
|
||||
|
||||
statusCode := errWithCode.Code()
|
||||
|
||||
if statusCode == http.StatusInternalServerError {
|
||||
l.Error("Internal Server Error")
|
||||
} else {
|
||||
l.Debug("handling error")
|
||||
}
|
||||
|
||||
// if we panic for any reason during error handling,
|
||||
// we should still try to return a basic code
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
l.Warnf("recovered from panic: %s", p)
|
||||
c.JSON(statusCode, gin.H{"error": errWithCode.Safe()})
|
||||
}
|
||||
}()
|
||||
|
||||
// discover if we're allowed to serve a nice html error page,
|
||||
// or if we should just use a json. Normally we would want to
|
||||
// check for a returned error, but if an error occurs here we
|
||||
// can just fall back to default behavior (serve json error).
|
||||
accept, _ := NegotiateAccept(c, HTMLOrJSONAcceptHeaders...)
|
||||
|
||||
if statusCode == http.StatusNotFound {
|
||||
// use our special not found handler with useful status text
|
||||
NotFoundHandler(c, instanceGet, accept)
|
||||
} else {
|
||||
genericErrorHandler(c, instanceGet, accept, errWithCode)
|
||||
}
|
||||
}
|
||||
|
||||
// OAuthErrorHandler is a lot like ErrorHandler, but it specifically returns errors
|
||||
// that are compatible with https://datatracker.ietf.org/doc/html/rfc6749#section-5.2,
|
||||
// but serializing errWithCode.Error() in the 'error' field, and putting any help text
|
||||
// from the error in the 'error_description' field. This means you should be careful not
|
||||
// to pass any detailed errors (that might contain sensitive information) into the
|
||||
// errWithCode.Error() field, since the client will see this. Use your noggin!
|
||||
func OAuthErrorHandler(c *gin.Context, errWithCode gtserror.WithCode) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"path": c.Request.URL.Path,
|
||||
"error": errWithCode.Error(),
|
||||
"help": errWithCode.Safe(),
|
||||
})
|
||||
|
||||
statusCode := errWithCode.Code()
|
||||
|
||||
if statusCode == http.StatusInternalServerError {
|
||||
l.Error("Internal Server Error")
|
||||
} else {
|
||||
l.Debug("handling OAuth error")
|
||||
}
|
||||
|
||||
c.JSON(statusCode, gin.H{
|
||||
"error": errWithCode.Error(),
|
||||
"error_description": errWithCode.Safe(),
|
||||
})
|
||||
}
|
34
internal/api/mime.go
Normal file
34
internal/api/mime.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
// MIME represents a mime-type.
|
||||
type MIME string
|
||||
|
||||
// MIME type
|
||||
const (
|
||||
AppJSON MIME = `application/json`
|
||||
AppXML MIME = `application/xml`
|
||||
AppActivityJSON MIME = `application/activity+json`
|
||||
AppActivityLDJSON MIME = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
|
||||
AppForm MIME = `application/x-www-form-urlencoded`
|
||||
MultipartForm MIME = `multipart/form-data`
|
||||
TextXML MIME = `text/xml`
|
||||
TextHTML MIME = `text/html`
|
||||
)
|
|
@ -43,3 +43,24 @@ type Notification struct {
|
|||
// Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls.
|
||||
Status *Status `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
/*
|
||||
The below functions are added onto the apimodel notification so that it satisfies
|
||||
the Timelineable interface in internal/timeline.
|
||||
*/
|
||||
|
||||
func (n *Notification) GetID() string {
|
||||
return n.ID
|
||||
}
|
||||
|
||||
func (n *Notification) GetAccountID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (n *Notification) GetBoostOfID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (n *Notification) GetBoostOfAccountID() string {
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -18,9 +18,11 @@
|
|||
|
||||
package model
|
||||
|
||||
// StatusTimelineResponse wraps a slice of statuses, ready to be serialized, along with the Link
|
||||
import "github.com/superseriousbusiness/gotosocial/internal/timeline"
|
||||
|
||||
// TimelineResponse wraps a slice of timelineables, ready to be serialized, along with the Link
|
||||
// header for the previous and next queries, to be returned to the client.
|
||||
type StatusTimelineResponse struct {
|
||||
Statuses []*Status
|
||||
type TimelineResponse struct {
|
||||
Items []timeline.Timelineable
|
||||
LinkHeader string
|
||||
}
|
||||
|
|
|
@ -25,33 +25,40 @@
|
|||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Offer represents an offered mime-type.
|
||||
type Offer string
|
||||
|
||||
const (
|
||||
AppJSON Offer = `application/json` // AppJSON is the mime type for 'application/json'.
|
||||
AppActivityJSON Offer = `application/activity+json` // AppActivityJSON is the mime type for 'application/activity+json'.
|
||||
AppActivityLDJSON Offer = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` // AppActivityLDJSON is the mime type for 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||
TextHTML Offer = `text/html` // TextHTML is the mime type for 'text/html'.
|
||||
)
|
||||
|
||||
// ActivityPubAcceptHeaders represents the Accept headers mentioned here:
|
||||
// https://www.w3.org/TR/activitypub/#retrieving-objects
|
||||
var ActivityPubAcceptHeaders = []Offer{
|
||||
//
|
||||
var ActivityPubAcceptHeaders = []MIME{
|
||||
AppActivityJSON,
|
||||
AppActivityLDJSON,
|
||||
}
|
||||
|
||||
// JSONAcceptHeaders is a slice of offers that just contains application/json types.
|
||||
var JSONAcceptHeaders = []Offer{
|
||||
var JSONAcceptHeaders = []MIME{
|
||||
AppJSON,
|
||||
}
|
||||
|
||||
// HTMLOrJSONAcceptHeaders is a slice of offers that prefers TextHTML and will
|
||||
// fall back to JSON if necessary. This is useful for error handling, since it can
|
||||
// be used to serve a nice HTML page if the caller accepts that, or just JSON if not.
|
||||
var HTMLOrJSONAcceptHeaders = []MIME{
|
||||
TextHTML,
|
||||
AppJSON,
|
||||
}
|
||||
|
||||
// HTMLAcceptHeaders is a slice of offers that just contains text/html types.
|
||||
var HTMLAcceptHeaders = []Offer{
|
||||
var HTMLAcceptHeaders = []MIME{
|
||||
TextHTML,
|
||||
}
|
||||
|
||||
// HTMLOrActivityPubHeaders matches text/html first, then activitypub types.
|
||||
// This is useful for user URLs that a user might go to in their browser.
|
||||
// https://www.w3.org/TR/activitypub/#retrieving-objects
|
||||
var HTMLOrActivityPubHeaders = []MIME{
|
||||
TextHTML,
|
||||
AppActivityJSON,
|
||||
AppActivityLDJSON,
|
||||
}
|
||||
|
||||
// NegotiateAccept takes the *gin.Context from an incoming request, and a
|
||||
// slice of Offers, and performs content negotiation for the given request
|
||||
// with the given content-type offers. It will return a string representation
|
||||
|
@ -73,7 +80,7 @@
|
|||
// often-used Accept types.
|
||||
//
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#server-driven_content_negotiation
|
||||
func NegotiateAccept(c *gin.Context, offers ...Offer) (string, error) {
|
||||
func NegotiateAccept(c *gin.Context, offers ...MIME) (string, error) {
|
||||
if len(offers) == 0 {
|
||||
return "", errors.New("no format offered")
|
||||
}
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// NodeInfoGETHandler swagger:operation GET /nodeinfo/2.0 nodeInfoGet
|
||||
|
@ -45,27 +45,22 @@
|
|||
// schema:
|
||||
// "$ref": "#/definitions/nodeinfo"
|
||||
func (m *Module) NodeInfoGETHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "NodeInfoGETHandler",
|
||||
"user-agent": c.Request.UserAgent(),
|
||||
})
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
ni, err := m.processor.GetNodeInfo(c.Request.Context(), c.Request)
|
||||
ni, errWithCode := m.processor.GetNodeInfo(c.Request.Context(), c.Request)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(ni)
|
||||
if err != nil {
|
||||
l.Debugf("error with get node info request: %s", err)
|
||||
c.JSON(err.Code(), err.Safe())
|
||||
api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
b, jsonErr := json.Marshal(ni)
|
||||
if jsonErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": jsonErr.Error()})
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, `application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"`, b)
|
||||
}
|
||||
|
|
|
@ -22,8 +22,8 @@
|
|||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// NodeInfoWellKnownGETHandler swagger:operation GET /.well-known/nodeinfo nodeInfoWellKnownGet
|
||||
|
@ -45,19 +45,14 @@
|
|||
// schema:
|
||||
// "$ref": "#/definitions/wellKnownResponse"
|
||||
func (m *Module) NodeInfoWellKnownGETHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "NodeInfoWellKnownGETHandler",
|
||||
})
|
||||
|
||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
niRel, err := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request)
|
||||
if err != nil {
|
||||
l.Debugf("error with get node info rel request: %s", err)
|
||||
c.JSON(err.Code(), err.Safe())
|
||||
niRel, errWithCode := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request)
|
||||
if errWithCode != nil {
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -20,48 +20,45 @@
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it.
|
||||
func (m *Module) FollowersGETHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "FollowersGETHandler",
|
||||
"url": c.Request.RequestURI,
|
||||
})
|
||||
|
||||
requestedUsername := c.Param(UsernameKey)
|
||||
// usernames on our instance are always lowercase
|
||||
requestedUsername := strings.ToLower(c.Param(UsernameKey))
|
||||
if requestedUsername == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
|
||||
err := errors.New("no username specified in request")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...)
|
||||
format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
l.Tracef("negotiated format: %s", format)
|
||||
|
||||
ctx := transferContext(c)
|
||||
if format == string(api.TextHTML) {
|
||||
// redirect to the user's profile
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
|
||||
}
|
||||
|
||||
followers, errWithCode := m.processor.GetFediFollowers(ctx, requestedUsername, c.Request.URL)
|
||||
resp, errWithCode := m.processor.GetFediFollowers(transferContext(c), requestedUsername, c.Request.URL)
|
||||
if errWithCode != nil {
|
||||
l.Info(errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
b, mErr := json.Marshal(followers)
|
||||
if mErr != nil {
|
||||
err := fmt.Errorf("could not marshal json: %s", mErr)
|
||||
l.Error(err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -20,48 +20,45 @@
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it.
|
||||
func (m *Module) FollowingGETHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "FollowingGETHandler",
|
||||
"url": c.Request.RequestURI,
|
||||
})
|
||||
|
||||
requestedUsername := c.Param(UsernameKey)
|
||||
// usernames on our instance are always lowercase
|
||||
requestedUsername := strings.ToLower(c.Param(UsernameKey))
|
||||
if requestedUsername == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
|
||||
err := errors.New("no username specified in request")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...)
|
||||
format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
||||
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
l.Tracef("negotiated format: %s", format)
|
||||
|
||||
ctx := transferContext(c)
|
||||
if format == string(api.TextHTML) {
|
||||
// redirect to the user's profile
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
|
||||
}
|
||||
|
||||
following, errWithCode := m.processor.GetFediFollowing(ctx, requestedUsername, c.Request.URL)
|
||||
resp, errWithCode := m.processor.GetFediFollowing(transferContext(c), requestedUsername, c.Request.URL)
|
||||
if errWithCode != nil {
|
||||
l.Info(errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
b, mErr := json.Marshal(following)
|
||||
if mErr != nil {
|
||||
err := fmt.Errorf("could not marshal json: %s", mErr)
|
||||
l.Error(err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -19,43 +19,33 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck
|
||||
)
|
||||
|
||||
// InboxPOSTHandler deals with incoming POST requests to an actor's inbox.
|
||||
// Eg., POST to https://example.org/users/whatever/inbox.
|
||||
func (m *Module) InboxPOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"func": "InboxPOSTHandler",
|
||||
"url": c.Request.RequestURI,
|
||||
})
|
||||
|
||||
requestedUsername := c.Param(UsernameKey)
|
||||
// usernames on our instance are always lowercase
|
||||
requestedUsername := strings.ToLower(c.Param(UsernameKey))
|
||||
if requestedUsername == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
|
||||
err := errors.New("no username specified in request")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := transferContext(c)
|
||||
|
||||
posted, err := m.processor.InboxPost(ctx, c.Writer, c.Request)
|
||||
if err != nil {
|
||||
if posted, err := m.processor.InboxPost(transferContext(c), c.Writer, c.Request); err != nil {
|
||||
if withCode, ok := err.(gtserror.WithCode); ok {
|
||||
l.Debugf("InboxPOSTHandler: %s", withCode.Error())
|
||||
c.JSON(withCode.Code(), withCode.Safe())
|
||||
return
|
||||
api.ErrorHandler(c, withCode, m.processor.InstanceGet)
|
||||
} else {
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
}
|
||||
l.Debugf("InboxPOSTHandler: error processing request: %s", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"})
|
||||
return
|
||||
}
|
||||
|
||||
if !posted {
|
||||
l.Debugf("InboxPOSTHandler: request could not be handled as an AP request; headers were: %+v", c.Request.Header)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"})
|
||||
} else if !posted {
|
||||
err := errors.New("unable to process request")
|
||||
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue