mirror of
https://github.com/versia-pub/server.git
synced 2025-12-08 17:28:19 +01:00
Compare commits
No commits in common. "main" and "v0.8.0-rc.0" have entirely different histories.
main
...
v0.8.0-rc.
|
|
@ -1,18 +1,16 @@
|
||||||
version = 1
|
version = 1
|
||||||
|
|
||||||
test_patterns = ["**/*.test.ts"]
|
|
||||||
|
|
||||||
[[analyzers]]
|
[[analyzers]]
|
||||||
name = "shell"
|
name = "shell"
|
||||||
|
|
||||||
[[analyzers]]
|
[[analyzers]]
|
||||||
name = "javascript"
|
name = "javascript"
|
||||||
|
|
||||||
[analyzers.meta]
|
[analyzers.meta]
|
||||||
environment = ["nodejs"]
|
environment = ["nodejs"]
|
||||||
|
|
||||||
[[analyzers]]
|
[[analyzers]]
|
||||||
name = "docker"
|
name = "docker"
|
||||||
|
|
||||||
[analyzers.meta]
|
[analyzers.meta]
|
||||||
dockerfile_paths = ["Dockerfile"]
|
dockerfile_paths = ["Dockerfile"]
|
||||||
9
.devcontainer/Dockerfile
Normal file
9
.devcontainer/Dockerfile
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Bun doesn't run well on Musl but this seems to work
|
||||||
|
FROM oven/bun:1.2.13-alpine as base
|
||||||
|
|
||||||
|
# Switch to Bash by editing /etc/passwd
|
||||||
|
RUN apk add --no-cache libstdc++ git bash curl openssh cloc && \
|
||||||
|
sed -i -e 's|/bin/ash|/bin/bash|g' /etc/passwd
|
||||||
|
|
||||||
|
# Extract Node from its docker image (node:22-alpine)
|
||||||
|
COPY --from=node:22-alpine /usr/local/bin/node /usr/local/bin/node
|
||||||
34
.devcontainer/devcontainer.json
Normal file
34
.devcontainer/devcontainer.json
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "versia Dev Container",
|
||||||
|
"dockerFile": "Dockerfile",
|
||||||
|
"runArgs": [
|
||||||
|
"-v",
|
||||||
|
"${localWorkspaceFolder}/config:/workspace/config",
|
||||||
|
"-v",
|
||||||
|
"${localWorkspaceFolder}/logs:/workspace/logs",
|
||||||
|
"-v",
|
||||||
|
"${localWorkspaceFolder}/uploads:/workspace/uploads",
|
||||||
|
"--network=host"
|
||||||
|
],
|
||||||
|
"mounts": [
|
||||||
|
"source=node_modules,target=/workspace/node_modules,type=bind,consistency=cached",
|
||||||
|
"type=bind,source=/home/${localEnv:USER}/.ssh,target=/root/.ssh,readonly"
|
||||||
|
],
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"settings": {
|
||||||
|
"terminal.integrated.shell.linux": "/bin/bash"
|
||||||
|
},
|
||||||
|
"extensions": [
|
||||||
|
"biomejs.biome",
|
||||||
|
"ms-vscode-remote.remote-containers",
|
||||||
|
"oven.bun-vscode",
|
||||||
|
"vivaxy.vscode-conventional-commits",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"tamasfe.even-better-toml",
|
||||||
|
"YoavBls.pretty-ts-errors",
|
||||||
|
"eamodio.gitlens"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
.github/config.workflow.toml
vendored
51
.github/config.workflow.toml
vendored
|
|
@ -429,32 +429,53 @@ text = "No spam"
|
||||||
|
|
||||||
[logging]
|
[logging]
|
||||||
|
|
||||||
# Available levels: trace, debug, info, warning, error, fatal
|
# Available levels: debug, info, warning, error, fatal
|
||||||
log_level = "info" # For console output
|
log_level = "debug"
|
||||||
|
|
||||||
|
log_file_path = "logs/versia.log"
|
||||||
|
|
||||||
|
[logging.types]
|
||||||
|
# Either pass a boolean
|
||||||
|
# requests = true
|
||||||
|
# Or a table with the following keys:
|
||||||
|
# requests_content = { level = "debug", log_file_path = "logs/requests.log" }
|
||||||
|
# Available types are: requests, responses, requests_content, filters
|
||||||
|
|
||||||
# [logging.file]
|
|
||||||
# path = "logs/versia.log"
|
|
||||||
# log_level = "info"
|
|
||||||
#
|
|
||||||
# [logging.file.rotation]
|
|
||||||
# max_size = 10_000_000 # 10 MB
|
|
||||||
# max_files = 10 # Keep 10 rotated files
|
|
||||||
#
|
|
||||||
# https://sentry.io support
|
# https://sentry.io support
|
||||||
|
# Uncomment to enable
|
||||||
# [logging.sentry]
|
# [logging.sentry]
|
||||||
|
# Sentry DSN for error logging
|
||||||
# dsn = "https://example.com"
|
# dsn = "https://example.com"
|
||||||
# debug = false
|
# debug = false
|
||||||
|
|
||||||
# sample_rate = 1.0
|
# sample_rate = 1.0
|
||||||
# traces_sample_rate = 1.0
|
# traces_sample_rate = 1.0
|
||||||
# Can also be regex
|
# Can also be regex
|
||||||
# trace_propagation_targets = []
|
# trace_propagation_targets = []
|
||||||
# max_breadcrumbs = 100
|
# max_breadcrumbs = 100
|
||||||
# environment = "production"
|
# environment = "production"
|
||||||
# log_level = "info"
|
|
||||||
|
|
||||||
[authentication]
|
[plugins]
|
||||||
# Run Versia Server with this value missing to generate a new key
|
# Whether to automatically load all plugins in the plugins directory
|
||||||
key = "ZWcwanRaQAqY3ChUro/Jey9XGQjzsxEed5iqTp4yFr8W6vEnXdz91F/Pu/uf7HBMbNeIK7V6aHsM0lq9onrO8Q=="
|
autoload = true
|
||||||
|
|
||||||
|
# Override for autoload
|
||||||
|
[plugins.overrides]
|
||||||
|
enabled = []
|
||||||
|
disabled = []
|
||||||
|
|
||||||
|
[plugins.config."@versia/openid"]
|
||||||
|
# If enabled, Versia will require users to log in with an OpenID provider
|
||||||
|
forced = false
|
||||||
|
|
||||||
|
# Allow registration with OpenID providers
|
||||||
|
# If signups.registration is false, it will only be possible to register with OpenID
|
||||||
|
allow_registration = true
|
||||||
|
|
||||||
|
[plugins.config."@versia/openid".keys]
|
||||||
|
# Run Versia Server with those values missing to generate a new key
|
||||||
|
public = "MCowBQYDK2VwAyEAfyZx8r98gVHtdH5EF1NYrBeChOXkt50mqiwKO2TX0f8="
|
||||||
|
private = "MC4CAQAwBQYDK2VwBCIEILDi1g7+bwNjBBvL4CRWHZpCFBR2m2OPCot62Wr+TCbq"
|
||||||
|
|
||||||
# The provider MUST support OpenID Connect with .well-known discovery
|
# The provider MUST support OpenID Connect with .well-known discovery
|
||||||
# Most notably, GitHub does not support this
|
# Most notably, GitHub does not support this
|
||||||
|
|
@ -463,7 +484,7 @@ key = "ZWcwanRaQAqY3ChUro/Jey9XGQjzsxEed5iqTp4yFr8W6vEnXdz91F/Pu/uf7HBMbNeIK7V6a
|
||||||
# The asterisk is important, as it allows for any query parameters to be passed
|
# The asterisk is important, as it allows for any query parameters to be passed
|
||||||
# Authentik for example uses regex so it can be set to (regex):
|
# Authentik for example uses regex so it can be set to (regex):
|
||||||
# <base_url>/oauth/sso/<provider_id>/callback.*
|
# <base_url>/oauth/sso/<provider_id>/callback.*
|
||||||
# [[authentication.openid_providers]]
|
# [[plugins.config."@versia/openid".providers]]
|
||||||
# name = "CPlusPatch ID"
|
# name = "CPlusPatch ID"
|
||||||
# id = "cpluspatch-id"
|
# id = "cpluspatch-id"
|
||||||
# This MUST match the provider's issuer URI, including the trailing slash (or lack thereof)
|
# This MUST match the provider's issuer URI, including the trailing slash (or lack thereof)
|
||||||
|
|
|
||||||
8
.github/copilot-instructions.md
vendored
8
.github/copilot-instructions.md
vendored
|
|
@ -13,10 +13,4 @@ const add = (a: number, b: number): number => a + b;
|
||||||
|
|
||||||
We always write TypeScript with double quotes and four spaces for indentation, so when your responses include TypeScript code, please follow those conventions.
|
We always write TypeScript with double quotes and four spaces for indentation, so when your responses include TypeScript code, please follow those conventions.
|
||||||
|
|
||||||
Our codebase uses Drizzle as an ORM, which is exposed in the `@versia-server/kit/db` and `@versia-server/kit/tables` packages. This project uses a monorepo structure with Bun as the package manager.
|
Our codebase uses Drizzle as an ORM, with custom abstractions in `classes/database/` for interacting with the database. The `@versia/kit/db` and `@versia/kit/tables` packages are aliases for these abstractions.
|
||||||
|
|
||||||
The app has two modes: worker and API. The worker mode is used for background tasks, while the API mode serves HTTP requests. The entry point for the worker is `worker.ts`, and for the API, it is `api.ts`.
|
|
||||||
|
|
||||||
Run the typechecker with `bun run typecheck` to ensure that all TypeScript code is type-checked correctly. Run tests with `bun test` to ensure that all tests pass. Run the linter and formatter with `bun lint` to ensure that the code adheres to our style guidelines, and `bun lint --write` to automatically fix minor/formatting issues.
|
|
||||||
|
|
||||||
Cover all new functionality with tests, and ensure that all tests pass before submitting your code.
|
|
||||||
|
|
|
||||||
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
|
|
@ -24,4 +24,4 @@ jobs:
|
||||||
|
|
||||||
- name: Run typechecks
|
- name: Run typechecks
|
||||||
run: |
|
run: |
|
||||||
bun run typecheck
|
bun run check
|
||||||
|
|
|
||||||
27
.github/workflows/circular-imports.yml
vendored
27
.github/workflows/circular-imports.yml
vendored
|
|
@ -1,27 +0,0 @@
|
||||||
name: Check Circular Imports
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install NPM packages
|
|
||||||
run: |
|
|
||||||
bun install
|
|
||||||
|
|
||||||
- name: Run typechecks
|
|
||||||
run: |
|
|
||||||
bun run detect-circular
|
|
||||||
5
.github/workflows/docker.yml
vendored
5
.github/workflows/docker.yml
vendored
|
|
@ -18,12 +18,9 @@ jobs:
|
||||||
tests:
|
tests:
|
||||||
uses: ./.github/workflows/tests.yml
|
uses: ./.github/workflows/tests.yml
|
||||||
|
|
||||||
detect-circular:
|
|
||||||
uses: ./.github/workflows/circular-imports.yml
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
needs: [lint, check, tests, detect-circular]
|
needs: [lint, check, tests]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
|
||||||
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
|
|
@ -35,12 +35,12 @@ jobs:
|
||||||
run: bun install
|
run: bun install
|
||||||
|
|
||||||
- name: Build with VitePress
|
- name: Build with VitePress
|
||||||
run: bun run --filter="@versia-server/api" docs:build
|
run: bun run docs:build
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: packages/api/docs/.vitepress/dist
|
path: docs/.vitepress/dist
|
||||||
|
|
||||||
# Deployment job
|
# Deployment job
|
||||||
deploy:
|
deploy:
|
||||||
|
|
|
||||||
48
.github/workflows/publish.yml
vendored
48
.github/workflows/publish.yml
vendored
|
|
@ -1,48 +0,0 @@
|
||||||
name: Build & Publish Packages
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
package:
|
|
||||||
description: "Package to publish"
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- client
|
|
||||||
- sdk
|
|
||||||
tag:
|
|
||||||
description: "NPM tag to use"
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
default: nightly
|
|
||||||
options:
|
|
||||||
- latest
|
|
||||||
- nightly
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
# For provenance generation
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment: NPM Deploy
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Publish to NPM
|
|
||||||
working-directory: packages/${{ inputs.package }}
|
|
||||||
run: bun publish --provenance --tag ${{ inputs.tag }} --access public
|
|
||||||
env:
|
|
||||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|
||||||
- name: Publish to JSR
|
|
||||||
working-directory: packages/${{ inputs.package }}
|
|
||||||
run: bunx jsr publish --allow-slow-types --allow-dirty
|
|
||||||
11
.github/workflows/test-publish.yml
vendored
11
.github/workflows/test-publish.yml
vendored
|
|
@ -23,13 +23,16 @@ jobs:
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
- name: Install
|
- name: Install
|
||||||
run: bun install --frozen-lockfile
|
run: bun install
|
||||||
|
|
||||||
|
- name: Configure .npmrc
|
||||||
|
working-directory: packages/${{ matrix.package }}
|
||||||
|
run: |
|
||||||
|
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
|
||||||
|
|
||||||
- name: Publish to NPM
|
- name: Publish to NPM
|
||||||
working-directory: packages/${{ matrix.package }}
|
working-directory: packages/${{ matrix.package }}
|
||||||
env:
|
run: bun publish --dry-run
|
||||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
run: bun publish --dry-run --access public
|
|
||||||
|
|
||||||
- name: Publish to JSR
|
- name: Publish to JSR
|
||||||
working-directory: packages/${{ matrix.package }}
|
working-directory: packages/${{ matrix.package }}
|
||||||
|
|
|
||||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
|
@ -6,10 +6,9 @@
|
||||||
"cli",
|
"cli",
|
||||||
"federation",
|
"federation",
|
||||||
"config",
|
"config",
|
||||||
|
"plugin",
|
||||||
"worker",
|
"worker",
|
||||||
"media",
|
"media"
|
||||||
"packages/client",
|
|
||||||
"packages/sdk"
|
|
||||||
],
|
],
|
||||||
"languageToolLinter.languageTool.ignoredWordsInWorkspace": ["versia"]
|
"languageToolLinter.languageTool.ignoredWordsInWorkspace": ["versia"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -1,16 +1,3 @@
|
||||||
# `0.9.0` (upcoming)
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### API
|
|
||||||
|
|
||||||
- [x] 🥺 Emoji Reactions are now available! You can react to any note with custom emojis.
|
|
||||||
- [x] 🔎 Added support for [batch account data API](https://docs.joinmastodon.org/methods/accounts/#index).
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
- [x] 🚀 Upgraded Bun to `1.3.2`
|
|
||||||
|
|
||||||
# `0.8.0` • Federation 2: Electric Boogaloo
|
# `0.8.0` • Federation 2: Electric Boogaloo
|
||||||
|
|
||||||
## Backwards Compatibility
|
## Backwards Compatibility
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ TypeScript errors should be ignored with `// @ts-expect-error` comments, as well
|
||||||
|
|
||||||
To scan for all TypeScript errors, run:
|
To scan for all TypeScript errors, run:
|
||||||
```sh
|
```sh
|
||||||
bun typecheck
|
bun check
|
||||||
```
|
```
|
||||||
|
|
||||||
### Commit messages
|
### Commit messages
|
||||||
|
|
|
||||||
15
Dockerfile
15
Dockerfile
|
|
@ -1,5 +1,7 @@
|
||||||
# Node is required for building the project
|
# Node is required for building the project
|
||||||
FROM imbios/bun-node:latest-23-alpine AS base
|
FROM imbios/bun-node:1-20-alpine AS base
|
||||||
|
|
||||||
|
RUN apk add --no-cache libstdc++
|
||||||
|
|
||||||
# Install dependencies into temp directory
|
# Install dependencies into temp directory
|
||||||
# This will cache them and speed up future builds
|
# This will cache them and speed up future builds
|
||||||
|
|
@ -20,19 +22,20 @@ COPY --from=install /temp/node_modules /temp/node_modules
|
||||||
|
|
||||||
# Build the project
|
# Build the project
|
||||||
WORKDIR /temp
|
WORKDIR /temp
|
||||||
RUN bun run build api
|
RUN bun run build
|
||||||
WORKDIR /temp/dist
|
WORKDIR /temp/dist
|
||||||
|
|
||||||
# Copy production dependencies and source code into final image
|
# Copy production dependencies and source code into final image
|
||||||
FROM oven/bun:1.3.2-alpine
|
FROM oven/bun:1.2.13-alpine
|
||||||
|
|
||||||
# Install libstdc++ for Bun and create app directory
|
# Install libstdc++ for Bun and create app directory
|
||||||
RUN mkdir -p /app
|
RUN apk add --no-cache libstdc++ && \
|
||||||
|
mkdir -p /app
|
||||||
|
|
||||||
COPY --from=build /temp/dist /app/dist
|
COPY --from=build /temp/dist /app/dist
|
||||||
COPY entrypoint.sh /app
|
COPY entrypoint.sh /app
|
||||||
|
|
||||||
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.com)"
|
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.dev)"
|
||||||
LABEL org.opencontainers.image.source="https://github.com/versia-pub/server"
|
LABEL org.opencontainers.image.source="https://github.com/versia-pub/server"
|
||||||
LABEL org.opencontainers.image.vendor="Versia Pub"
|
LABEL org.opencontainers.image.vendor="Versia Pub"
|
||||||
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
|
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
|
||||||
|
|
@ -48,4 +51,4 @@ WORKDIR /app
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENTRYPOINT [ "/bin/sh", "/app/entrypoint.sh" ]
|
ENTRYPOINT [ "/bin/sh", "/app/entrypoint.sh" ]
|
||||||
# Run migrations and start the server
|
# Run migrations and start the server
|
||||||
CMD [ "bun", "run", "api.js" ]
|
CMD [ "bun", "run", "index.js" ]
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,6 @@ The following extensions are currently supported or being worked on:
|
||||||
- `pub.versia:instance_messaging`: Instance Messaging
|
- `pub.versia:instance_messaging`: Instance Messaging
|
||||||
- `pub.versia:likes`: Likes
|
- `pub.versia:likes`: Likes
|
||||||
- `pub.versia:share`: Share
|
- `pub.versia:share`: Share
|
||||||
- `pub.versia:reactions`: Reactions
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
# Node is required for building the project
|
# Node is required for building the project
|
||||||
FROM imbios/bun-node:latest-23-alpine AS base
|
FROM imbios/bun-node:1-20-alpine AS base
|
||||||
|
|
||||||
|
RUN apk add --no-cache libstdc++
|
||||||
|
|
||||||
# Install dependencies into temp directory
|
# Install dependencies into temp directory
|
||||||
# This will cache them and speed up future builds
|
# This will cache them and speed up future builds
|
||||||
|
|
@ -20,19 +22,20 @@ COPY --from=install /temp/node_modules /temp/node_modules
|
||||||
|
|
||||||
# Build the project
|
# Build the project
|
||||||
WORKDIR /temp
|
WORKDIR /temp
|
||||||
RUN bun run build worker
|
RUN bun run build:worker
|
||||||
WORKDIR /temp/dist
|
WORKDIR /temp/dist
|
||||||
|
|
||||||
# Copy production dependencies and source code into final image
|
# Copy production dependencies and source code into final image
|
||||||
FROM oven/bun:1.3.2-alpine
|
FROM oven/bun:1.2.13-alpine
|
||||||
|
|
||||||
# Install libstdc++ for Bun and create app directory
|
# Install libstdc++ for Bun and create app directory
|
||||||
RUN mkdir -p /app
|
RUN apk add --no-cache libstdc++ && \
|
||||||
|
mkdir -p /app
|
||||||
|
|
||||||
COPY --from=build /temp/dist /app/dist
|
COPY --from=build /temp/dist /app/dist
|
||||||
COPY entrypoint.sh /app
|
COPY entrypoint.sh /app
|
||||||
|
|
||||||
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.com)"
|
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.dev)"
|
||||||
LABEL org.opencontainers.image.source="https://github.com/versia-pub/server"
|
LABEL org.opencontainers.image.source="https://github.com/versia-pub/server"
|
||||||
LABEL org.opencontainers.image.vendor="Versia Pub"
|
LABEL org.opencontainers.image.vendor="Versia Pub"
|
||||||
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
|
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
|
||||||
|
|
@ -44,8 +47,7 @@ ARG GIT_COMMIT
|
||||||
ENV GIT_COMMIT=$GIT_COMMIT
|
ENV GIT_COMMIT=$GIT_COMMIT
|
||||||
|
|
||||||
# CD to app
|
# CD to app
|
||||||
WORKDIR /app
|
WORKDIR /app/dist
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENTRYPOINT [ "/bin/sh", "/app/entrypoint.sh" ]
|
|
||||||
# Run migrations and start the server
|
# Run migrations and start the server
|
||||||
CMD [ "bun", "run", "worker.js" ]
|
CMD [ "bun", "run", "worker.js" ]
|
||||||
|
|
|
||||||
19
api.ts
19
api.ts
|
|
@ -1,19 +0,0 @@
|
||||||
import process from "node:process";
|
|
||||||
import { appFactory } from "@versia-server/api";
|
|
||||||
import { config } from "@versia-server/config";
|
|
||||||
import { Youch } from "youch";
|
|
||||||
import { createServer } from "@/server.ts";
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
process.exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("uncaughtException", async (error) => {
|
|
||||||
const youch = new Youch();
|
|
||||||
|
|
||||||
console.error(await youch.toANSI(error));
|
|
||||||
});
|
|
||||||
|
|
||||||
await import("@versia-server/api/setup");
|
|
||||||
|
|
||||||
createServer(config, await appFactory());
|
|
||||||
229
api/api/auth/login/index.test.ts
Normal file
229
api/api/auth/login/index.test.ts
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
|
import { Application } from "@versia/kit/db";
|
||||||
|
import { randomUUIDv7 } from "bun";
|
||||||
|
import { randomString } from "@/math";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
|
const { users, deleteUsers, passwords } = await getTestUsers(1);
|
||||||
|
|
||||||
|
// Create application
|
||||||
|
const application = await Application.insert({
|
||||||
|
id: randomUUIDv7(),
|
||||||
|
name: "Test Application",
|
||||||
|
clientId: randomString(32, "hex"),
|
||||||
|
secret: "test",
|
||||||
|
redirectUri: "https://example.com",
|
||||||
|
scopes: "read write",
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await deleteUsers();
|
||||||
|
await application.delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
// /api/auth/login
|
||||||
|
describe("/api/auth/login", () => {
|
||||||
|
test("should get a JWT with email", async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("identifier", users[0]?.data.email ?? "");
|
||||||
|
formData.append("password", passwords[0]);
|
||||||
|
|
||||||
|
const response = await fakeRequest(
|
||||||
|
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(302);
|
||||||
|
expect(response.headers.get("location")).toBeDefined();
|
||||||
|
const locationHeader = new URL(
|
||||||
|
response.headers.get("Location") ?? "",
|
||||||
|
config.http.base_url,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(locationHeader.pathname).toBe("/oauth/consent");
|
||||||
|
expect(locationHeader.searchParams.get("client_id")).toBe(
|
||||||
|
application.data.clientId,
|
||||||
|
);
|
||||||
|
expect(locationHeader.searchParams.get("redirect_uri")).toBe(
|
||||||
|
"https://example.com",
|
||||||
|
);
|
||||||
|
expect(locationHeader.searchParams.get("response_type")).toBe("code");
|
||||||
|
expect(locationHeader.searchParams.get("scope")).toBe("read write");
|
||||||
|
|
||||||
|
expect(response.headers.get("Set-Cookie")).toMatch(/jwt=[^;]+;/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should get a JWT with username", async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("identifier", users[0]?.data.username ?? "");
|
||||||
|
formData.append("password", passwords[0]);
|
||||||
|
|
||||||
|
const response = await fakeRequest(
|
||||||
|
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(302);
|
||||||
|
expect(response.headers.get("location")).toBeDefined();
|
||||||
|
const locationHeader = new URL(
|
||||||
|
response.headers.get("Location") ?? "",
|
||||||
|
config.http.base_url,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(locationHeader.pathname).toBe("/oauth/consent");
|
||||||
|
expect(locationHeader.searchParams.get("client_id")).toBe(
|
||||||
|
application.data.clientId,
|
||||||
|
);
|
||||||
|
expect(locationHeader.searchParams.get("redirect_uri")).toBe(
|
||||||
|
"https://example.com",
|
||||||
|
);
|
||||||
|
expect(locationHeader.searchParams.get("response_type")).toBe("code");
|
||||||
|
expect(locationHeader.searchParams.get("scope")).toBe("read write");
|
||||||
|
|
||||||
|
expect(response.headers.get("Set-Cookie")).toMatch(/jwt=[^;]+;/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should have state in the URL", async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("identifier", users[0]?.data.email ?? "");
|
||||||
|
formData.append("password", passwords[0]);
|
||||||
|
|
||||||
|
const response = await fakeRequest(
|
||||||
|
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write&state=abc`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(302);
|
||||||
|
expect(response.headers.get("location")).toBeDefined();
|
||||||
|
const locationHeader = new URL(
|
||||||
|
response.headers.get("Location") ?? "",
|
||||||
|
config.http.base_url,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(locationHeader.pathname).toBe("/oauth/consent");
|
||||||
|
expect(locationHeader.searchParams.get("client_id")).toBe(
|
||||||
|
application.data.clientId,
|
||||||
|
);
|
||||||
|
expect(locationHeader.searchParams.get("redirect_uri")).toBe(
|
||||||
|
"https://example.com",
|
||||||
|
);
|
||||||
|
expect(locationHeader.searchParams.get("response_type")).toBe("code");
|
||||||
|
expect(locationHeader.searchParams.get("scope")).toBe("read write");
|
||||||
|
expect(locationHeader.searchParams.get("state")).toBe("abc");
|
||||||
|
|
||||||
|
expect(response.headers.get("Set-Cookie")).toMatch(/jwt=[^;]+;/);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("should reject invalid credentials", () => {
|
||||||
|
// Redirects to /oauth/authorize on invalid
|
||||||
|
test("invalid email", async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("identifier", "ababa@gmail.com");
|
||||||
|
formData.append("password", "password");
|
||||||
|
|
||||||
|
const response = await fakeRequest(
|
||||||
|
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
|
||||||
|
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(302);
|
||||||
|
expect(response.headers.get("location")).toBeDefined();
|
||||||
|
const locationHeader = new URL(
|
||||||
|
response.headers.get("Location") ?? "",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(locationHeader.pathname).toBe("/oauth/authorize");
|
||||||
|
expect(locationHeader.searchParams.get("error")).toBe(
|
||||||
|
"invalid_grant",
|
||||||
|
);
|
||||||
|
expect(locationHeader.searchParams.get("error_description")).toBe(
|
||||||
|
"Invalid identifier or password",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.headers.get("Set-Cookie")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid username", async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("identifier", "ababa");
|
||||||
|
formData.append("password", "password");
|
||||||
|
|
||||||
|
const response = await fakeRequest(
|
||||||
|
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(302);
|
||||||
|
expect(response.headers.get("location")).toBeDefined();
|
||||||
|
const locationHeader = new URL(
|
||||||
|
response.headers.get("Location") ?? "",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(locationHeader.pathname).toBe("/oauth/authorize");
|
||||||
|
expect(locationHeader.searchParams.get("error")).toBe(
|
||||||
|
"invalid_grant",
|
||||||
|
);
|
||||||
|
expect(locationHeader.searchParams.get("error_description")).toBe(
|
||||||
|
"Invalid identifier or password",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.headers.get("Set-Cookie")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid password", async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("identifier", users[0]?.data.email ?? "");
|
||||||
|
formData.append("password", "password");
|
||||||
|
|
||||||
|
const response = await fakeRequest(
|
||||||
|
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(302);
|
||||||
|
expect(response.headers.get("location")).toBeDefined();
|
||||||
|
const locationHeader = new URL(
|
||||||
|
response.headers.get("Location") ?? "",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(locationHeader.pathname).toBe("/oauth/authorize");
|
||||||
|
expect(locationHeader.searchParams.get("error")).toBe(
|
||||||
|
"invalid_grant",
|
||||||
|
);
|
||||||
|
expect(locationHeader.searchParams.get("error_description")).toBe(
|
||||||
|
"Invalid identifier or password",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.headers.get("Set-Cookie")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
231
api/api/auth/login/index.ts
Normal file
231
api/api/auth/login/index.ts
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
import { Application, User } from "@versia/kit/db";
|
||||||
|
import { Users } from "@versia/kit/tables";
|
||||||
|
import { password as bunPassword } from "bun";
|
||||||
|
import { eq, or } from "drizzle-orm";
|
||||||
|
import type { Context } from "hono";
|
||||||
|
import { setCookie } from "hono/cookie";
|
||||||
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { validator } from "hono-openapi/zod";
|
||||||
|
import { SignJWT } from "jose";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { apiRoute, handleZodError } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
|
const returnError = (
|
||||||
|
context: Context,
|
||||||
|
error: string,
|
||||||
|
description: string,
|
||||||
|
): Response => {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
// Add all data that is not undefined except email and password
|
||||||
|
for (const [key, value] of Object.entries(context.req.query())) {
|
||||||
|
if (key !== "email" && key !== "password" && value !== undefined) {
|
||||||
|
searchParams.append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchParams.append("error", error);
|
||||||
|
searchParams.append("error_description", description);
|
||||||
|
|
||||||
|
return context.redirect(
|
||||||
|
new URL(
|
||||||
|
`${config.frontend.routes.login}?${searchParams.toString()}`,
|
||||||
|
config.http.base_url,
|
||||||
|
).toString(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default apiRoute((app) =>
|
||||||
|
app.post(
|
||||||
|
"/api/auth/login",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Login",
|
||||||
|
description: "Login to the application",
|
||||||
|
responses: {
|
||||||
|
302: {
|
||||||
|
description: "Redirect to OAuth authorize, or error",
|
||||||
|
headers: {
|
||||||
|
"Set-Cookie": {
|
||||||
|
description: "JWT cookie",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator(
|
||||||
|
"query",
|
||||||
|
z.object({
|
||||||
|
scope: z.string().optional(),
|
||||||
|
redirect_uri: z.string().url().optional(),
|
||||||
|
response_type: z.enum([
|
||||||
|
"code",
|
||||||
|
"token",
|
||||||
|
"none",
|
||||||
|
"id_token",
|
||||||
|
"code id_token",
|
||||||
|
"code token",
|
||||||
|
"token id_token",
|
||||||
|
"code token id_token",
|
||||||
|
]),
|
||||||
|
client_id: z.string(),
|
||||||
|
state: z.string().optional(),
|
||||||
|
code_challenge: z.string().optional(),
|
||||||
|
code_challenge_method: z.enum(["plain", "S256"]).optional(),
|
||||||
|
prompt: z
|
||||||
|
.enum(["none", "login", "consent", "select_account"])
|
||||||
|
.optional()
|
||||||
|
.default("none"),
|
||||||
|
max_age: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.optional()
|
||||||
|
.default(60 * 60 * 24 * 7),
|
||||||
|
}),
|
||||||
|
handleZodError,
|
||||||
|
),
|
||||||
|
validator(
|
||||||
|
"form",
|
||||||
|
z.object({
|
||||||
|
identifier: z
|
||||||
|
.string()
|
||||||
|
.email()
|
||||||
|
.toLowerCase()
|
||||||
|
.or(z.string().toLowerCase()),
|
||||||
|
password: z.string().min(2).max(100),
|
||||||
|
}),
|
||||||
|
handleZodError,
|
||||||
|
),
|
||||||
|
async (context) => {
|
||||||
|
const oidcConfig = config.plugins?.config?.["@versia/openid"] as
|
||||||
|
| {
|
||||||
|
forced: boolean;
|
||||||
|
providers: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
}[];
|
||||||
|
keys: {
|
||||||
|
private: string;
|
||||||
|
public: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (!oidcConfig) {
|
||||||
|
return returnError(
|
||||||
|
context,
|
||||||
|
"invalid_request",
|
||||||
|
"The OpenID Connect plugin is not enabled on this instance. Cannot process login request.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oidcConfig?.forced) {
|
||||||
|
return returnError(
|
||||||
|
context,
|
||||||
|
"invalid_request",
|
||||||
|
"Logging in with a password is disabled by the administrator. Please use a valid OpenID Connect provider.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { identifier, password } = context.req.valid("form");
|
||||||
|
const { client_id } = context.req.valid("query");
|
||||||
|
|
||||||
|
// Find user
|
||||||
|
const user = await User.fromSql(
|
||||||
|
or(
|
||||||
|
eq(Users.email, identifier.toLowerCase()),
|
||||||
|
eq(Users.username, identifier.toLowerCase()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
user &&
|
||||||
|
(await bunPassword.verify(
|
||||||
|
password,
|
||||||
|
user.data.password || "",
|
||||||
|
))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return returnError(
|
||||||
|
context,
|
||||||
|
"invalid_grant",
|
||||||
|
"Invalid identifier or password",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.data.passwordResetToken) {
|
||||||
|
return context.redirect(
|
||||||
|
`${config.frontend.routes.password_reset}?${new URLSearchParams(
|
||||||
|
{
|
||||||
|
token: user.data.passwordResetToken ?? "",
|
||||||
|
login_reset: "true",
|
||||||
|
},
|
||||||
|
).toString()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try and import the key
|
||||||
|
const privateKey = await crypto.subtle.importKey(
|
||||||
|
"pkcs8",
|
||||||
|
Buffer.from(oidcConfig?.keys?.private ?? "", "base64"),
|
||||||
|
"Ed25519",
|
||||||
|
false,
|
||||||
|
["sign"],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate JWT
|
||||||
|
const jwt = await new SignJWT({
|
||||||
|
sub: user.id,
|
||||||
|
iss: config.http.base_url.origin,
|
||||||
|
aud: client_id,
|
||||||
|
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
||||||
|
iat: Math.floor(Date.now() / 1000),
|
||||||
|
nbf: Math.floor(Date.now() / 1000),
|
||||||
|
})
|
||||||
|
.setProtectedHeader({ alg: "EdDSA" })
|
||||||
|
.sign(privateKey);
|
||||||
|
|
||||||
|
const application = await Application.fromClientId(client_id);
|
||||||
|
|
||||||
|
if (!application) {
|
||||||
|
throw new ApiError(400, "Invalid application");
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
application: application.data.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (application.data.website) {
|
||||||
|
searchParams.append("website", application.data.website);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all data that is not undefined except email and password
|
||||||
|
for (const [key, value] of Object.entries(context.req.query())) {
|
||||||
|
if (
|
||||||
|
key !== "email" &&
|
||||||
|
key !== "password" &&
|
||||||
|
value !== undefined
|
||||||
|
) {
|
||||||
|
searchParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to OAuth authorize with JWT
|
||||||
|
setCookie(context, "jwt", jwt, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: "Strict",
|
||||||
|
path: "/",
|
||||||
|
// 2 weeks
|
||||||
|
maxAge: 60 * 60 * 24 * 14,
|
||||||
|
});
|
||||||
|
return context.redirect(
|
||||||
|
`${config.frontend.routes.consent}?${searchParams.toString()}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
76
api/api/auth/redirect/index.ts
Normal file
76
api/api/auth/redirect/index.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { db } from "@versia/kit/db";
|
||||||
|
import { Applications, Tokens } from "@versia/kit/tables";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { validator } from "hono-openapi/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { apiRoute, handleZodError } from "@/api";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth Code flow
|
||||||
|
*/
|
||||||
|
export default apiRoute((app) =>
|
||||||
|
app.get(
|
||||||
|
"/api/auth/redirect",
|
||||||
|
describeRoute({
|
||||||
|
summary: "OAuth Code flow",
|
||||||
|
description:
|
||||||
|
"Redirects to the application, or back to login if the code is invalid",
|
||||||
|
tags: ["OpenID"],
|
||||||
|
responses: {
|
||||||
|
302: {
|
||||||
|
description:
|
||||||
|
"Redirects to the application, or back to login if the code is invalid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator(
|
||||||
|
"query",
|
||||||
|
z.object({
|
||||||
|
redirect_uri: z.string().url(),
|
||||||
|
client_id: z.string(),
|
||||||
|
code: z.string(),
|
||||||
|
}),
|
||||||
|
handleZodError,
|
||||||
|
),
|
||||||
|
async (context) => {
|
||||||
|
const { redirect_uri, client_id, code } =
|
||||||
|
context.req.valid("query");
|
||||||
|
|
||||||
|
const redirectToLogin = (error: string): Response =>
|
||||||
|
context.redirect(
|
||||||
|
`${config.frontend.routes.login}?${new URLSearchParams({
|
||||||
|
...context.req.query,
|
||||||
|
error: encodeURIComponent(error),
|
||||||
|
}).toString()}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const foundToken = await db
|
||||||
|
.select()
|
||||||
|
.from(Tokens)
|
||||||
|
.leftJoin(
|
||||||
|
Applications,
|
||||||
|
eq(Tokens.applicationId, Applications.id),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(Tokens.code, code),
|
||||||
|
eq(Applications.clientId, client_id),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!foundToken || foundToken.length <= 0) {
|
||||||
|
return redirectToLogin("Invalid code");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect back to application
|
||||||
|
return context.redirect(
|
||||||
|
`${redirect_uri}?${new URLSearchParams({
|
||||||
|
code,
|
||||||
|
}).toString()}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
124
api/api/auth/reset/index.test.ts
Normal file
124
api/api/auth/reset/index.test.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
|
import { Application } from "@versia/kit/db";
|
||||||
|
import { randomUUIDv7 } from "bun";
|
||||||
|
import { randomString } from "@/math";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
|
const { users, deleteUsers, passwords } = await getTestUsers(1);
|
||||||
|
const token = randomString(32, "hex");
|
||||||
|
const newPassword = randomString(16, "hex");
|
||||||
|
|
||||||
|
// Create application
|
||||||
|
const application = await Application.insert({
|
||||||
|
id: randomUUIDv7(),
|
||||||
|
name: "Test Application",
|
||||||
|
clientId: randomString(32, "hex"),
|
||||||
|
secret: "test",
|
||||||
|
redirectUri: "https://example.com",
|
||||||
|
scopes: "read write",
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await deleteUsers();
|
||||||
|
await application.delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
// /api/auth/reset
|
||||||
|
describe("/api/auth/reset", () => {
|
||||||
|
test("should login with normal password", async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("identifier", users[0]?.data.username ?? "");
|
||||||
|
formData.append("password", passwords[0]);
|
||||||
|
|
||||||
|
const response = await fakeRequest(
|
||||||
|
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
|
||||||
|
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(302);
|
||||||
|
expect(response.headers.get("location")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reset password and refuse login with old password", async () => {
|
||||||
|
await users[0]?.update({
|
||||||
|
passwordResetToken: token,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("identifier", users[0]?.data.username ?? "");
|
||||||
|
formData.append("password", passwords[0]);
|
||||||
|
|
||||||
|
const response = await fakeRequest(
|
||||||
|
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(302);
|
||||||
|
expect(response.headers.get("location")).toBeDefined();
|
||||||
|
const locationHeader = new URL(
|
||||||
|
response.headers.get("Location") ?? "",
|
||||||
|
config.http.base_url,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(locationHeader.pathname).toBe("/oauth/reset");
|
||||||
|
expect(locationHeader.searchParams.get("token")).toBe(token);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reset password and login with new password", async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("token", token);
|
||||||
|
formData.append("password", newPassword);
|
||||||
|
formData.append("password2", newPassword);
|
||||||
|
|
||||||
|
const response = await fakeRequest("/api/auth/reset", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(302);
|
||||||
|
expect(response.headers.get("location")).toBeDefined();
|
||||||
|
|
||||||
|
const loginFormData = new FormData();
|
||||||
|
|
||||||
|
loginFormData.append("identifier", users[0]?.data.username ?? "");
|
||||||
|
loginFormData.append("password", newPassword);
|
||||||
|
|
||||||
|
const loginResponse = await fakeRequest(
|
||||||
|
`/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: loginFormData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(loginResponse.status).toBe(302);
|
||||||
|
expect(loginResponse.headers.get("location")).toBeDefined();
|
||||||
|
const locationHeader = new URL(
|
||||||
|
loginResponse.headers.get("Location") ?? "",
|
||||||
|
config.http.base_url,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(locationHeader.pathname).toBe("/oauth/consent");
|
||||||
|
expect(locationHeader.searchParams.get("client_id")).toBe(
|
||||||
|
application.data.clientId,
|
||||||
|
);
|
||||||
|
expect(locationHeader.searchParams.get("redirect_uri")).toBe(
|
||||||
|
"https://example.com",
|
||||||
|
);
|
||||||
|
expect(locationHeader.searchParams.get("response_type")).toBe("code");
|
||||||
|
expect(locationHeader.searchParams.get("scope")).toBe("read write");
|
||||||
|
|
||||||
|
expect(loginResponse.headers.get("Set-Cookie")).toMatch(/jwt=[^;]+;/);
|
||||||
|
});
|
||||||
|
});
|
||||||
81
api/api/auth/reset/index.ts
Normal file
81
api/api/auth/reset/index.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { User } from "@versia/kit/db";
|
||||||
|
import { Users } from "@versia/kit/tables";
|
||||||
|
import { password as bunPassword } from "bun";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { Context } from "hono";
|
||||||
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { validator } from "hono-openapi/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { apiRoute, handleZodError } from "@/api";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
|
const returnError = (
|
||||||
|
context: Context,
|
||||||
|
token: string,
|
||||||
|
error: string,
|
||||||
|
description: string,
|
||||||
|
): Response => {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
searchParams.append("error", error);
|
||||||
|
searchParams.append("error_description", description);
|
||||||
|
searchParams.append("token", token);
|
||||||
|
|
||||||
|
return context.redirect(
|
||||||
|
new URL(
|
||||||
|
`${
|
||||||
|
config.frontend.routes.password_reset
|
||||||
|
}?${searchParams.toString()}`,
|
||||||
|
config.http.base_url,
|
||||||
|
).toString(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default apiRoute((app) =>
|
||||||
|
app.post(
|
||||||
|
"/api/auth/reset",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Reset password",
|
||||||
|
description: "Reset password",
|
||||||
|
responses: {
|
||||||
|
302: {
|
||||||
|
description:
|
||||||
|
"Redirect to the password reset page with a message",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator(
|
||||||
|
"form",
|
||||||
|
z.object({
|
||||||
|
token: z.string().min(1),
|
||||||
|
password: z.string().min(3).max(100),
|
||||||
|
}),
|
||||||
|
handleZodError,
|
||||||
|
),
|
||||||
|
async (context) => {
|
||||||
|
const { token, password } = context.req.valid("form");
|
||||||
|
|
||||||
|
const user = await User.fromSql(
|
||||||
|
eq(Users.passwordResetToken, token),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return returnError(
|
||||||
|
context,
|
||||||
|
token,
|
||||||
|
"invalid_token",
|
||||||
|
"Invalid token",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.update({
|
||||||
|
password: await bunPassword.hash(password),
|
||||||
|
passwordResetToken: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return context.redirect(
|
||||||
|
`${config.frontend.routes.password_reset}?success=true`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
|
|
||||||
|
|
@ -2,10 +2,11 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { Relationship } from "@versia-server/kit/db";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { apiRoute, auth, withUserParam } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
import { RolePermission } from "@versia/client/schemas";
|
import { RolePermission } from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { describeRoute } from "hono-openapi";
|
||||||
import {
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
apiRoute,
|
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
withUserParam,
|
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||||
import { getFeed } from "@/rss";
|
import { getFeed } from "@/rss";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -38,13 +34,12 @@ export default apiRoute((app) =>
|
||||||
RolePermission.ViewNotes,
|
RolePermission.ViewNotes,
|
||||||
RolePermission.ViewAccounts,
|
RolePermission.ViewAccounts,
|
||||||
],
|
],
|
||||||
|
|
||||||
scopes: ["read:statuses"],
|
scopes: ["read:statuses"],
|
||||||
}),
|
}),
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
page: z.coerce.number().default(0).meta({
|
page: z.coerce.number().default(0).openapi({
|
||||||
description: "Page number to fetch. Defaults to 0.",
|
description: "Page number to fetch. Defaults to 0.",
|
||||||
example: 2,
|
example: 2,
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
import { RolePermission } from "@versia/client/schemas";
|
import { RolePermission } from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { describeRoute } from "hono-openapi";
|
||||||
import {
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
apiRoute,
|
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
withUserParam,
|
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||||
import { getFeed } from "@/rss";
|
import { getFeed } from "@/rss";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -37,13 +33,12 @@ export default apiRoute((app) =>
|
||||||
RolePermission.ViewNotes,
|
RolePermission.ViewNotes,
|
||||||
RolePermission.ViewAccounts,
|
RolePermission.ViewAccounts,
|
||||||
],
|
],
|
||||||
|
|
||||||
scopes: ["read:statuses"],
|
scopes: ["read:statuses"],
|
||||||
}),
|
}),
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
page: z.coerce.number().default(0).meta({
|
page: z.coerce.number().default(0).openapi({
|
||||||
description: "Page number to fetch. Defaults to 0.",
|
description: "Page number to fetch. Defaults to 0.",
|
||||||
example: 2,
|
example: 2,
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(3);
|
const { users, deleteUsers } = await getTestUsers(3);
|
||||||
|
|
||||||
|
|
@ -3,16 +3,12 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import {
|
import { describeRoute } from "hono-openapi";
|
||||||
apiRoute,
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
withUserParam,
|
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { Relationship } from "@versia-server/kit/db";
|
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -61,12 +57,12 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"json",
|
"json",
|
||||||
z.object({
|
z.object({
|
||||||
reblogs: z.boolean().default(true).meta({
|
reblogs: z.boolean().default(true).openapi({
|
||||||
description:
|
description:
|
||||||
"Receive this account’s reblogs in home timeline?",
|
"Receive this account’s reblogs in home timeline?",
|
||||||
example: true,
|
example: true,
|
||||||
}),
|
}),
|
||||||
notify: z.boolean().default(false).meta({
|
notify: z.boolean().default(false).openapi({
|
||||||
description:
|
description:
|
||||||
"Receive notifications when this account posts a status?",
|
"Receive notifications when this account posts a status?",
|
||||||
example: false,
|
example: false,
|
||||||
|
|
@ -74,7 +70,7 @@ export default apiRoute((app) =>
|
||||||
languages: z
|
languages: z
|
||||||
.array(iso631)
|
.array(iso631)
|
||||||
.default([])
|
.default([])
|
||||||
.meta({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"Array of String (ISO 639-1 language two-letter code). Filter received statuses for these languages. If not provided, you will receive this account’s posts in all languages.",
|
"Array of String (ISO 639-1 language two-letter code). Filter received statuses for these languages. If not provided, you will receive this account’s posts in all languages.",
|
||||||
example: ["en", "fr"],
|
example: ["en", "fr"],
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(5);
|
const { users, deleteUsers } = await getTestUsers(5);
|
||||||
|
|
||||||
|
|
@ -2,18 +2,14 @@ import {
|
||||||
Account as AccountSchema,
|
Account as AccountSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Timeline } from "@versia/kit/db";
|
||||||
import {
|
import { Users } from "@versia/kit/tables";
|
||||||
apiRoute,
|
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
withUserParam,
|
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { Timeline } from "@versia-server/kit/db";
|
|
||||||
import { Users } from "@versia-server/kit/tables";
|
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -38,7 +34,7 @@ export default apiRoute((app) =>
|
||||||
link: z
|
link: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.meta({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"Links to the next and previous pages",
|
"Links to the next and previous pages",
|
||||||
example:
|
example:
|
||||||
|
|
@ -65,22 +61,22 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
max_id: AccountSchema.shape.id.optional().meta({
|
max_id: AccountSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||||
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||||
}),
|
}),
|
||||||
since_id: AccountSchema.shape.id.optional().meta({
|
since_id: AccountSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
min_id: AccountSchema.shape.id.optional().meta({
|
min_id: AccountSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
limit: z.number().int().min(1).max(40).default(20).meta({
|
limit: z.number().int().min(1).max(40).default(20).openapi({
|
||||||
description: "Maximum number of results to return.",
|
description: "Maximum number of results to return.",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(5);
|
const { users, deleteUsers } = await getTestUsers(5);
|
||||||
|
|
||||||
|
|
@ -2,18 +2,14 @@ import {
|
||||||
Account as AccountSchema,
|
Account as AccountSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Timeline } from "@versia/kit/db";
|
||||||
import {
|
import { Users } from "@versia/kit/tables";
|
||||||
apiRoute,
|
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
withUserParam,
|
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { Timeline } from "@versia-server/kit/db";
|
|
||||||
import { Users } from "@versia-server/kit/tables";
|
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -39,7 +35,7 @@ export default apiRoute((app) =>
|
||||||
link: z
|
link: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.meta({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"Links to the next and previous pages",
|
"Links to the next and previous pages",
|
||||||
example:
|
example:
|
||||||
|
|
@ -66,22 +62,22 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
max_id: AccountSchema.shape.id.optional().meta({
|
max_id: AccountSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||||
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||||
}),
|
}),
|
||||||
since_id: AccountSchema.shape.id.optional().meta({
|
since_id: AccountSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
min_id: AccountSchema.shape.id.optional().meta({
|
min_id: AccountSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
limit: z.number().int().min(1).max(40).default(20).meta({
|
limit: z.number().int().min(1).max(40).default(20).openapi({
|
||||||
description: "Maximum number of results to return.",
|
description: "Maximum number of results to return.",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import {
|
import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
|
||||||
generateClient,
|
|
||||||
getTestStatuses,
|
|
||||||
getTestUsers,
|
|
||||||
} from "@versia-server/tests";
|
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(5);
|
const { users, deleteUsers } = await getTestUsers(5);
|
||||||
const timeline = (await getTestStatuses(5, users[0])).toReversed();
|
const timeline = (await getTestStatuses(5, users[0])).toReversed();
|
||||||
|
|
@ -2,9 +2,10 @@ import {
|
||||||
Account as AccountSchema,
|
Account as AccountSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { apiRoute, auth, withUserParam } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
|
|
||||||
|
|
@ -2,20 +2,16 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import {
|
import { describeRoute } from "hono-openapi";
|
||||||
apiRoute,
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
auth,
|
import { z } from "zod";
|
||||||
handleZodError,
|
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||||
withUserParam,
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { Relationship } from "@versia-server/kit/db";
|
|
||||||
import {
|
import {
|
||||||
RelationshipJobType,
|
RelationshipJobType,
|
||||||
relationshipQueue,
|
relationshipQueue,
|
||||||
} from "@versia-server/kit/queues/relationships";
|
} from "~/classes/queues/relationships";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -55,7 +51,7 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"json",
|
"json",
|
||||||
z.object({
|
z.object({
|
||||||
notifications: z.boolean().default(true).meta({
|
notifications: z.boolean().default(true).openapi({
|
||||||
description: "Mute notifications in addition to statuses?",
|
description: "Mute notifications in addition to statuses?",
|
||||||
}),
|
}),
|
||||||
duration: z
|
duration: z
|
||||||
|
|
@ -64,7 +60,7 @@ export default apiRoute((app) =>
|
||||||
.min(0)
|
.min(0)
|
||||||
.max(60 * 60 * 24 * 365 * 5)
|
.max(60 * 60 * 24 * 365 * 5)
|
||||||
.default(0)
|
.default(0)
|
||||||
.meta({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"How long the mute should last, in seconds. 0 means indefinite.",
|
"How long the mute should last, in seconds. 0 means indefinite.",
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
|
|
||||||
|
|
@ -2,16 +2,12 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import {
|
import { describeRoute } from "hono-openapi";
|
||||||
apiRoute,
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
withUserParam,
|
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { Relationship } from "@versia-server/kit/db";
|
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -49,7 +45,7 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"json",
|
"json",
|
||||||
z.object({
|
z.object({
|
||||||
comment: RelationshipSchema.shape.note.optional().meta({
|
comment: RelationshipSchema.shape.note.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"The comment to be set on that user. Provide an empty string or leave out this parameter to clear the currently set note.",
|
"The comment to be set on that user. Provide an empty string or leave out this parameter to clear the currently set note.",
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
|
|
||||||
|
|
@ -2,9 +2,10 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { Relationship } from "@versia-server/kit/db";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { resolver } from "hono-openapi/zod";
|
||||||
|
import { apiRoute, auth, withUserParam } from "@/api";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -2,10 +2,11 @@ import {
|
||||||
Account as AccountSchema,
|
Account as AccountSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { User } from "@versia-server/kit/db";
|
import { apiRoute, auth, withUserParam } from "@/api";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { User } from "~/classes/database/user";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
|
|
||||||
|
|
@ -2,10 +2,11 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { Relationship } from "@versia-server/kit/db";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { apiRoute, auth, withUserParam } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { RolePermission } from "@versia/client/schemas";
|
import { RolePermission } from "@versia/client/schemas";
|
||||||
import { Role } from "@versia-server/kit/db";
|
import { Role } from "@versia/kit/db";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
let role: Role;
|
let role: Role;
|
||||||
|
|
@ -3,16 +3,12 @@ import {
|
||||||
RolePermission,
|
RolePermission,
|
||||||
Role as RoleSchema,
|
Role as RoleSchema,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Role } from "@versia/kit/db";
|
||||||
import {
|
import { describeRoute } from "hono-openapi";
|
||||||
apiRoute,
|
import { validator } from "hono-openapi/zod";
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
withUserParam,
|
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { Role } from "@versia-server/kit/db";
|
|
||||||
import { describeRoute, validator } from "hono-openapi";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { RolePermission } from "@versia/client/schemas";
|
import { RolePermission } from "@versia/client/schemas";
|
||||||
import { Role } from "@versia-server/kit/db";
|
import { Role } from "@versia/kit/db";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
let role: Role;
|
let role: Role;
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { Role as RoleSchema } from "@versia/client/schemas";
|
import { Role as RoleSchema } from "@versia/client/schemas";
|
||||||
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
import { Role } from "@versia/kit/db";
|
||||||
import { Role } from "@versia-server/kit/db";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, withUserParam } from "@/api";
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -3,7 +3,7 @@ import {
|
||||||
generateClient,
|
generateClient,
|
||||||
getTestStatuses,
|
getTestStatuses,
|
||||||
getTestUsers,
|
getTestUsers,
|
||||||
} from "@versia-server/tests";
|
} from "~/tests/utils.ts";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(5);
|
const { users, deleteUsers } = await getTestUsers(5);
|
||||||
const timeline = (await getTestStatuses(5, users[1])).toReversed();
|
const timeline = (await getTestStatuses(5, users[1])).toReversed();
|
||||||
|
|
@ -3,18 +3,14 @@ import {
|
||||||
Status as StatusSchema,
|
Status as StatusSchema,
|
||||||
zBoolean,
|
zBoolean,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Timeline } from "@versia/kit/db";
|
||||||
import {
|
import { Notes } from "@versia/kit/tables";
|
||||||
apiRoute,
|
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
withUserParam,
|
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { Timeline } from "@versia-server/kit/db";
|
|
||||||
import { Notes } from "@versia-server/kit/tables";
|
|
||||||
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -46,45 +42,50 @@ export default apiRoute((app) =>
|
||||||
RolePermission.ViewNotes,
|
RolePermission.ViewNotes,
|
||||||
RolePermission.ViewAccounts,
|
RolePermission.ViewAccounts,
|
||||||
],
|
],
|
||||||
|
|
||||||
scopes: ["read:statuses"],
|
scopes: ["read:statuses"],
|
||||||
}),
|
}),
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
max_id: StatusSchema.shape.id.optional().meta({
|
max_id: StatusSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||||
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||||
}),
|
}),
|
||||||
since_id: StatusSchema.shape.id.optional().meta({
|
since_id: StatusSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
min_id: StatusSchema.shape.id.optional().meta({
|
min_id: StatusSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
limit: z.coerce.number().int().min(1).max(40).default(20).meta({
|
limit: z.coerce
|
||||||
description: "Maximum number of results to return.",
|
.number()
|
||||||
}),
|
.int()
|
||||||
only_media: zBoolean.default(false).meta({
|
.min(1)
|
||||||
|
.max(40)
|
||||||
|
.default(20)
|
||||||
|
.openapi({
|
||||||
|
description: "Maximum number of results to return.",
|
||||||
|
}),
|
||||||
|
only_media: zBoolean.default(false).openapi({
|
||||||
description: "Filter out statuses without attachments.",
|
description: "Filter out statuses without attachments.",
|
||||||
}),
|
}),
|
||||||
exclude_replies: zBoolean.default(false).meta({
|
exclude_replies: zBoolean.default(false).openapi({
|
||||||
description:
|
description:
|
||||||
"Filter out statuses in reply to a different account.",
|
"Filter out statuses in reply to a different account.",
|
||||||
}),
|
}),
|
||||||
exclude_reblogs: zBoolean.default(false).meta({
|
exclude_reblogs: zBoolean.default(false).openapi({
|
||||||
description: "Filter out boosts from the response.",
|
description: "Filter out boosts from the response.",
|
||||||
}),
|
}),
|
||||||
pinned: zBoolean.default(false).meta({
|
pinned: zBoolean.default(false).openapi({
|
||||||
description:
|
description:
|
||||||
"Filter for pinned statuses only. Pinned statuses do not receive special priority in the order of the returned results.",
|
"Filter for pinned statuses only. Pinned statuses do not receive special priority in the order of the returned results.",
|
||||||
}),
|
}),
|
||||||
tagged: z.string().optional().meta({
|
tagged: z.string().optional().openapi({
|
||||||
description:
|
description:
|
||||||
"Filter for statuses using a specific hashtag.",
|
"Filter for statuses using a specific hashtag.",
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
|
|
||||||
|
|
@ -2,10 +2,11 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { Relationship } from "@versia-server/kit/db";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { apiRoute, auth, withUserParam } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(3);
|
const { users, deleteUsers } = await getTestUsers(3);
|
||||||
|
|
||||||
|
|
@ -2,10 +2,11 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { Relationship } from "@versia-server/kit/db";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { apiRoute, auth, withUserParam } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
|
|
||||||
|
|
@ -2,10 +2,11 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { Relationship } from "@versia-server/kit/db";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { apiRoute, auth, withUserParam } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
|
|
||||||
|
|
@ -2,10 +2,11 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { Relationship } from "@versia-server/kit/db";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { apiRoute, auth, withUserParam } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils.ts";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(5);
|
const { users, deleteUsers } = await getTestUsers(5);
|
||||||
|
|
||||||
|
|
@ -3,19 +3,15 @@ import {
|
||||||
FamiliarFollowers as FamiliarFollowersSchema,
|
FamiliarFollowers as FamiliarFollowersSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { db, User } from "@versia/kit/db";
|
||||||
import {
|
import type { Users } from "@versia/kit/tables";
|
||||||
apiRoute,
|
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
qsQuery,
|
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { db, User } from "@versia-server/kit/db";
|
|
||||||
import type { Users } from "@versia-server/kit/tables";
|
|
||||||
import { type InferSelectModel, sql } from "drizzle-orm";
|
import { type InferSelectModel, sql } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
import { apiRoute, auth, handleZodError, qsQuery } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { rateLimit } from "~/middlewares/rate-limit";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -55,8 +51,8 @@ export default apiRoute((app) =>
|
||||||
.array(AccountSchema.shape.id)
|
.array(AccountSchema.shape.id)
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(10)
|
.max(10)
|
||||||
.or(AccountSchema.shape.id)
|
.or(AccountSchema.shape.id.transform((v) => [v]))
|
||||||
.meta({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"Find familiar followers for the provided account IDs.",
|
"Find familiar followers for the provided account IDs.",
|
||||||
example: [
|
example: [
|
||||||
|
|
@ -69,11 +65,11 @@ export default apiRoute((app) =>
|
||||||
),
|
),
|
||||||
async (context) => {
|
async (context) => {
|
||||||
const { user } = context.get("auth");
|
const { user } = context.get("auth");
|
||||||
const { id } = context.req.valid("query");
|
const { id: ids } = context.req.valid("query");
|
||||||
|
|
||||||
// Find followers of the accounts in "ids", that you also follow
|
// Find followers of the accounts in "ids", that you also follow
|
||||||
const finalUsers = await Promise.all(
|
const finalUsers = await Promise.all(
|
||||||
(Array.isArray(id) ? id : [id]).map(async (id) => ({
|
ids.map(async (id) => ({
|
||||||
id,
|
id,
|
||||||
accounts: await User.fromIds(
|
accounts: await User.fromIds(
|
||||||
(
|
(
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { afterEach, describe, expect, test } from "bun:test";
|
import { afterEach, describe, expect, test } from "bun:test";
|
||||||
import { db } from "@versia-server/kit/db";
|
import { db } from "@versia/kit/db";
|
||||||
import { Users } from "@versia-server/kit/tables";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { generateClient, getSolvedChallenge } from "@versia-server/tests";
|
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { randomString } from "@/math";
|
import { randomString } from "@/math";
|
||||||
|
import { generateClient, getSolvedChallenge } from "~/tests/utils";
|
||||||
|
|
||||||
const username = randomString(10, "hex");
|
const username = randomString(10, "hex");
|
||||||
const username2 = randomString(10, "hex");
|
const username2 = randomString(10, "hex");
|
||||||
|
|
@ -1,114 +1,48 @@
|
||||||
import { Account as AccountSchema, zBoolean } from "@versia/client/schemas";
|
import { zBoolean } from "@versia/client/schemas";
|
||||||
import { config } from "@versia-server/config";
|
import { User } from "@versia/kit/db";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Users } from "@versia/kit/tables";
|
||||||
import {
|
|
||||||
apiRoute,
|
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
jsonOrForm,
|
|
||||||
qsQuery,
|
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { User } from "@versia-server/kit/db";
|
|
||||||
import { searchManager } from "@versia-server/kit/search";
|
|
||||||
import { Users } from "@versia-server/kit/tables";
|
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import ISO6391 from "iso-639-1";
|
import ISO6391 from "iso-639-1";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError, jsonOrForm } from "@/api";
|
||||||
import { tempmailDomains } from "@/tempmail";
|
import { tempmailDomains } from "@/tempmail";
|
||||||
import { rateLimit } from "../../../../middlewares/rate-limit.ts";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
import { rateLimit } from "~/middlewares/rate-limit";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
username: z.string().meta({
|
username: z.string().openapi({
|
||||||
description: "The desired username for the account",
|
description: "The desired username for the account",
|
||||||
example: "alice",
|
example: "alice",
|
||||||
}),
|
}),
|
||||||
email: z.string().toLowerCase().meta({
|
email: z.string().toLowerCase().openapi({
|
||||||
description:
|
description:
|
||||||
"The email address to be used for login. Transformed to lowercase.",
|
"The email address to be used for login. Transformed to lowercase.",
|
||||||
example: "alice@gmail.com",
|
example: "alice@gmail.com",
|
||||||
}),
|
}),
|
||||||
password: z.string().meta({
|
password: z.string().openapi({
|
||||||
description: "The password to be used for login",
|
description: "The password to be used for login",
|
||||||
example: "hunter2",
|
example: "hunter2",
|
||||||
}),
|
}),
|
||||||
agreement: zBoolean.meta({
|
agreement: zBoolean.openapi({
|
||||||
description:
|
description:
|
||||||
"Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE.",
|
"Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE.",
|
||||||
example: true,
|
example: true,
|
||||||
}),
|
}),
|
||||||
locale: z.string().meta({
|
locale: z.string().openapi({
|
||||||
description:
|
description:
|
||||||
"The language of the confirmation email that will be sent. ISO 639-1 code.",
|
"The language of the confirmation email that will be sent. ISO 639-1 code.",
|
||||||
example: "en",
|
example: "en",
|
||||||
}),
|
}),
|
||||||
reason: z.string().optional().meta({
|
reason: z.string().optional().openapi({
|
||||||
description:
|
description:
|
||||||
"If registrations require manual approval, this text will be reviewed by moderators.",
|
"If registrations require manual approval, this text will be reviewed by moderators.",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) =>
|
||||||
app.get(
|
|
||||||
"/api/v1/accounts",
|
|
||||||
describeRoute({
|
|
||||||
summary: "Get multiple accounts",
|
|
||||||
description: "View information about multiple profiles.",
|
|
||||||
externalDocs: {
|
|
||||||
url: "https://docs.joinmastodon.org/methods/accounts/#index",
|
|
||||||
},
|
|
||||||
tags: ["Accounts"],
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description:
|
|
||||||
"Account records for the requested confirmed and approved accounts. There can be fewer records than requested if the accounts do not exist or are not confirmed.",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(z.array(AccountSchema)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
422: ApiError.validationFailed().schema,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
qsQuery(),
|
|
||||||
auth({
|
|
||||||
auth: false,
|
|
||||||
scopes: [],
|
|
||||||
challenge: false,
|
|
||||||
}),
|
|
||||||
rateLimit(40),
|
|
||||||
validator(
|
|
||||||
"query",
|
|
||||||
z.object({
|
|
||||||
id: z
|
|
||||||
.array(AccountSchema.shape.id)
|
|
||||||
.min(1)
|
|
||||||
.max(40)
|
|
||||||
.or(AccountSchema.shape.id)
|
|
||||||
.meta({
|
|
||||||
description: "The IDs of the Accounts in the database.",
|
|
||||||
example: [
|
|
||||||
"f137ce6f-ff5e-4998-b20f-0361ba9be007",
|
|
||||||
"8424c654-5d03-4a1b-bec8-4e87db811b5d",
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
handleZodError,
|
|
||||||
),
|
|
||||||
async (context) => {
|
|
||||||
const { id } = context.req.valid("query");
|
|
||||||
|
|
||||||
// Find accounts by IDs
|
|
||||||
const accounts = await User.fromIds(Array.isArray(id) ? id : [id]);
|
|
||||||
|
|
||||||
return context.json(
|
|
||||||
accounts.map((account) => account.toApi()),
|
|
||||||
200,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
app.post(
|
app.post(
|
||||||
"/api/v1/accounts",
|
"/api/v1/accounts",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
|
|
@ -419,15 +353,12 @@ export default apiRoute((app) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.register(username, {
|
await User.register(username, {
|
||||||
password,
|
password,
|
||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add to search index
|
|
||||||
await searchManager.addUser(user);
|
|
||||||
|
|
||||||
return context.text("", 200);
|
return context.text("", 200);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(5);
|
const { users, deleteUsers } = await getTestUsers(5);
|
||||||
|
|
||||||
|
|
@ -2,16 +2,16 @@ import {
|
||||||
Account as AccountSchema,
|
Account as AccountSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { config } from "@versia-server/config";
|
import { Instance, User } from "@versia/kit/db";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
|
||||||
import { Instance, User } from "@versia-server/kit/db";
|
|
||||||
import { parseUserAddress } from "@versia-server/kit/parsers";
|
|
||||||
import { Users } from "@versia-server/kit/tables";
|
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
import { apiRoute, auth, handleZodError, parseUserAddress } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
import { rateLimit } from "~/middlewares/rate-limit";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -42,7 +42,7 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
acct: AccountSchema.shape.acct.meta({
|
acct: AccountSchema.shape.acct.openapi({
|
||||||
description: "The username or Webfinger address to lookup.",
|
description: "The username or Webfinger address to lookup.",
|
||||||
example: "lexi@beta.versia.social",
|
example: "lexi@beta.versia.social",
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { db } from "@versia-server/kit/db";
|
import { db } from "@versia/kit/db";
|
||||||
import { Users } from "@versia-server/kit/tables";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(5);
|
const { users, deleteUsers } = await getTestUsers(5);
|
||||||
|
|
||||||
|
|
@ -4,17 +4,13 @@ import {
|
||||||
RolePermission,
|
RolePermission,
|
||||||
zBoolean,
|
zBoolean,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import {
|
import { describeRoute } from "hono-openapi";
|
||||||
apiRoute,
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
qsQuery,
|
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { Relationship } from "@versia-server/kit/db";
|
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
import { apiRoute, auth, handleZodError, qsQuery } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { rateLimit } from "~/middlewares/rate-limit";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -54,8 +50,8 @@ export default apiRoute((app) =>
|
||||||
.array(AccountSchema.shape.id)
|
.array(AccountSchema.shape.id)
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(10)
|
.max(10)
|
||||||
.or(AccountSchema.shape.id)
|
.or(AccountSchema.shape.id.transform((v) => [v]))
|
||||||
.meta({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"Check relationships for the provided account IDs.",
|
"Check relationships for the provided account IDs.",
|
||||||
example: [
|
example: [
|
||||||
|
|
@ -63,7 +59,7 @@ export default apiRoute((app) =>
|
||||||
"8424c654-5d03-4a1b-bec8-4e87db811b5d",
|
"8424c654-5d03-4a1b-bec8-4e87db811b5d",
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
with_suspended: zBoolean.default(false).meta({
|
with_suspended: zBoolean.default(false).openapi({
|
||||||
description:
|
description:
|
||||||
"Whether relationships should be returned for suspended users",
|
"Whether relationships should be returned for suspended users",
|
||||||
example: false,
|
example: false,
|
||||||
|
|
@ -77,14 +73,17 @@ export default apiRoute((app) =>
|
||||||
// TODO: Implement with_suspended
|
// TODO: Implement with_suspended
|
||||||
const { id } = context.req.valid("query");
|
const { id } = context.req.valid("query");
|
||||||
|
|
||||||
|
const ids = Array.isArray(id) ? id : [id];
|
||||||
|
|
||||||
const relationships = await Relationship.fromOwnerAndSubjects(
|
const relationships = await Relationship.fromOwnerAndSubjects(
|
||||||
user,
|
user,
|
||||||
Array.isArray(id) ? id : [id],
|
ids,
|
||||||
);
|
);
|
||||||
|
|
||||||
relationships.sort(
|
relationships.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
id.indexOf(a.data.subjectId) - id.indexOf(b.data.subjectId),
|
ids.indexOf(a.data.subjectId) -
|
||||||
|
ids.indexOf(b.data.subjectId),
|
||||||
);
|
);
|
||||||
|
|
||||||
return context.json(
|
return context.json(
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(5);
|
const { users, deleteUsers } = await getTestUsers(5);
|
||||||
|
|
||||||
|
|
@ -3,16 +3,16 @@ import {
|
||||||
RolePermission,
|
RolePermission,
|
||||||
zBoolean,
|
zBoolean,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { User } from "@versia/kit/db";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { User } from "@versia-server/kit/db";
|
|
||||||
import { parseUserAddress } from "@versia-server/kit/parsers";
|
|
||||||
import { Users } from "@versia-server/kit/tables";
|
|
||||||
import { eq, ilike, not, or, sql } from "drizzle-orm";
|
import { eq, ilike, not, or, sql } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import stringComparison from "string-comparison";
|
import stringComparison from "string-comparison";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
import { apiRoute, auth, handleZodError, parseUserAddress } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { rateLimit } from "~/middlewares/rate-limit";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -47,24 +47,30 @@ export default apiRoute((app) =>
|
||||||
z.object({
|
z.object({
|
||||||
q: AccountSchema.shape.username
|
q: AccountSchema.shape.username
|
||||||
.or(AccountSchema.shape.acct)
|
.or(AccountSchema.shape.acct)
|
||||||
.meta({
|
.openapi({
|
||||||
description: "Search query for accounts.",
|
description: "Search query for accounts.",
|
||||||
example: "username",
|
example: "username",
|
||||||
}),
|
}),
|
||||||
limit: z.coerce.number().int().min(1).max(80).default(40).meta({
|
limit: z.coerce
|
||||||
description: "Maximum number of results.",
|
.number()
|
||||||
example: 40,
|
.int()
|
||||||
}),
|
.min(1)
|
||||||
offset: z.coerce.number().int().default(0).meta({
|
.max(80)
|
||||||
|
.default(40)
|
||||||
|
.openapi({
|
||||||
|
description: "Maximum number of results.",
|
||||||
|
example: 40,
|
||||||
|
}),
|
||||||
|
offset: z.coerce.number().int().default(0).openapi({
|
||||||
description: "Skip the first n results.",
|
description: "Skip the first n results.",
|
||||||
example: 0,
|
example: 0,
|
||||||
}),
|
}),
|
||||||
resolve: zBoolean.default(false).meta({
|
resolve: zBoolean.default(false).openapi({
|
||||||
description:
|
description:
|
||||||
"Attempt WebFinger lookup. Use this when q is an exact address.",
|
"Attempt WebFinger lookup. Use this when q is an exact address.",
|
||||||
example: false,
|
example: false,
|
||||||
}),
|
}),
|
||||||
following: zBoolean.default(false).meta({
|
following: zBoolean.default(false).openapi({
|
||||||
description: "Limit the search to users you are following.",
|
description: "Limit the search to users you are following.",
|
||||||
example: false,
|
example: false,
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { config } from "@versia-server/config";
|
import { config } from "~/config.ts";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(1);
|
const { users, deleteUsers } = await getTestUsers(1);
|
||||||
|
|
||||||
|
|
@ -3,24 +3,20 @@ import {
|
||||||
RolePermission,
|
RolePermission,
|
||||||
zBoolean,
|
zBoolean,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import * as VersiaEntities from "@versia/sdk/entities";
|
import { Emoji, Media, User } from "@versia/kit/db";
|
||||||
import { config } from "@versia-server/config";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { ApiError } from "@versia-server/kit";
|
|
||||||
import {
|
|
||||||
apiRoute,
|
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
jsonOrForm,
|
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { Emoji, Media, User } from "@versia-server/kit/db";
|
|
||||||
import { versiaTextToHtml } from "@versia-server/kit/parsers";
|
|
||||||
import { Users } from "@versia-server/kit/tables";
|
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError, jsonOrForm } from "@/api";
|
||||||
import { mergeAndDeduplicate } from "@/lib";
|
import { mergeAndDeduplicate } from "@/lib";
|
||||||
import { sanitizedHtmlStrip } from "@/sanitization";
|
import { sanitizedHtmlStrip } from "@/sanitization";
|
||||||
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { contentToHtml } from "~/classes/functions/status";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
import { rateLimit } from "~/middlewares/rate-limit";
|
||||||
|
import * as VersiaEntities from "~/packages/sdk/entities";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.patch(
|
app.patch(
|
||||||
|
|
@ -57,7 +53,7 @@ export default apiRoute((app) =>
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
display_name: AccountSchema.shape.display_name
|
display_name: AccountSchema.shape.display_name
|
||||||
.meta({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"The display name to use for the profile.",
|
"The display name to use for the profile.",
|
||||||
example: "Lexi",
|
example: "Lexi",
|
||||||
|
|
@ -74,7 +70,7 @@ export default apiRoute((app) =>
|
||||||
"Display name contains blocked words",
|
"Display name contains blocked words",
|
||||||
),
|
),
|
||||||
username: AccountSchema.shape.username
|
username: AccountSchema.shape.username
|
||||||
.meta({
|
.openapi({
|
||||||
description: "The username to use for the profile.",
|
description: "The username to use for the profile.",
|
||||||
example: "lexi",
|
example: "lexi",
|
||||||
})
|
})
|
||||||
|
|
@ -94,7 +90,7 @@ export default apiRoute((app) =>
|
||||||
"Username is disallowed",
|
"Username is disallowed",
|
||||||
),
|
),
|
||||||
note: AccountSchema.shape.note
|
note: AccountSchema.shape.note
|
||||||
.meta({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"The account bio. Markdown is supported.",
|
"The account bio. Markdown is supported.",
|
||||||
})
|
})
|
||||||
|
|
@ -107,60 +103,72 @@ export default apiRoute((app) =>
|
||||||
"Bio contains blocked words",
|
"Bio contains blocked words",
|
||||||
),
|
),
|
||||||
avatar: z
|
avatar: z
|
||||||
|
.string()
|
||||||
.url()
|
.url()
|
||||||
.meta({
|
.transform((a) => new URL(a))
|
||||||
|
.openapi({
|
||||||
description: "Avatar image URL",
|
description: "Avatar image URL",
|
||||||
})
|
})
|
||||||
.or(
|
.or(
|
||||||
z
|
z
|
||||||
.file()
|
.instanceof(File)
|
||||||
.max(
|
.refine(
|
||||||
config.validation.accounts.max_avatar_bytes,
|
(v) =>
|
||||||
|
v.size <=
|
||||||
|
config.validation.accounts
|
||||||
|
.max_avatar_bytes,
|
||||||
|
`Avatar must be less than ${config.validation.accounts.max_avatar_bytes} bytes`,
|
||||||
)
|
)
|
||||||
.meta({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"Avatar image encoded using multipart/form-data",
|
"Avatar image encoded using multipart/form-data",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
header: z
|
header: z
|
||||||
|
.string()
|
||||||
.url()
|
.url()
|
||||||
.meta({
|
.transform((v) => new URL(v))
|
||||||
|
.openapi({
|
||||||
description: "Header image URL",
|
description: "Header image URL",
|
||||||
})
|
})
|
||||||
.or(
|
.or(
|
||||||
z
|
z
|
||||||
.file()
|
.instanceof(File)
|
||||||
.max(
|
.refine(
|
||||||
config.validation.accounts.max_header_bytes,
|
(v) =>
|
||||||
|
v.size <=
|
||||||
|
config.validation.accounts
|
||||||
|
.max_header_bytes,
|
||||||
|
`Header must be less than ${config.validation.accounts.max_header_bytes} bytes`,
|
||||||
)
|
)
|
||||||
.meta({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"Header image encoded using multipart/form-data",
|
"Header image encoded using multipart/form-data",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
locked: AccountSchema.shape.locked.meta({
|
locked: AccountSchema.shape.locked.openapi({
|
||||||
description:
|
description:
|
||||||
"Whether manual approval of follow requests is required.",
|
"Whether manual approval of follow requests is required.",
|
||||||
}),
|
}),
|
||||||
bot: AccountSchema.shape.bot.meta({
|
bot: AccountSchema.shape.bot.openapi({
|
||||||
description: "Whether the account has a bot flag.",
|
description: "Whether the account has a bot flag.",
|
||||||
}),
|
}),
|
||||||
discoverable: AccountSchema.shape.discoverable
|
discoverable: AccountSchema.shape.discoverable
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.meta({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"Whether the account should be shown in the profile directory.",
|
"Whether the account should be shown in the profile directory.",
|
||||||
}),
|
}),
|
||||||
hide_collections: zBoolean.meta({
|
hide_collections: zBoolean.openapi({
|
||||||
description:
|
description:
|
||||||
"Whether to hide followers and followed accounts.",
|
"Whether to hide followers and followed accounts.",
|
||||||
}),
|
}),
|
||||||
indexable: zBoolean.meta({
|
indexable: zBoolean.openapi({
|
||||||
description:
|
description:
|
||||||
"Whether public posts should be searchable to anyone.",
|
"Whether public posts should be searchable to anyone.",
|
||||||
}),
|
}),
|
||||||
// TODO: Implement :(
|
// TODO: Implement :(
|
||||||
attribution_domains: z.array(z.string()).meta({
|
attribution_domains: z.array(z.string()).openapi({
|
||||||
description:
|
description:
|
||||||
"Domains of websites allowed to credit the account.",
|
"Domains of websites allowed to credit the account.",
|
||||||
example: ["cnn.com", "myblog.com"],
|
example: ["cnn.com", "myblog.com"],
|
||||||
|
|
@ -234,7 +242,7 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
if (note) {
|
if (note) {
|
||||||
self.source.note = note;
|
self.source.note = note;
|
||||||
self.note = await versiaTextToHtml(
|
self.note = await contentToHtml(
|
||||||
new VersiaEntities.TextContentFormat({
|
new VersiaEntities.TextContentFormat({
|
||||||
"text/markdown": {
|
"text/markdown": {
|
||||||
content: note,
|
content: note,
|
||||||
|
|
@ -274,9 +282,9 @@ export default apiRoute((app) =>
|
||||||
user.avatar = await Media.fromFile(avatar);
|
user.avatar = await Media.fromFile(avatar);
|
||||||
}
|
}
|
||||||
} else if (user.avatar) {
|
} else if (user.avatar) {
|
||||||
await user.avatar.updateFromUrl(new URL(avatar));
|
await user.avatar.updateFromUrl(avatar);
|
||||||
} else {
|
} else {
|
||||||
user.avatar = await Media.fromUrl(new URL(avatar));
|
user.avatar = await Media.fromUrl(avatar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,9 +296,9 @@ export default apiRoute((app) =>
|
||||||
user.header = await Media.fromFile(header);
|
user.header = await Media.fromFile(header);
|
||||||
}
|
}
|
||||||
} else if (user.header) {
|
} else if (user.header) {
|
||||||
await user.header.updateFromUrl(new URL(header));
|
await user.header.updateFromUrl(header);
|
||||||
} else {
|
} else {
|
||||||
user.header = await Media.fromUrl(new URL(header));
|
user.header = await Media.fromUrl(header);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,7 +329,7 @@ export default apiRoute((app) =>
|
||||||
self.source.fields = [];
|
self.source.fields = [];
|
||||||
for (const field of fields_attributes) {
|
for (const field of fields_attributes) {
|
||||||
// Can be Markdown or plaintext, also has emojis
|
// Can be Markdown or plaintext, also has emojis
|
||||||
const parsedName = await versiaTextToHtml(
|
const parsedName = await contentToHtml(
|
||||||
new VersiaEntities.TextContentFormat({
|
new VersiaEntities.TextContentFormat({
|
||||||
"text/markdown": {
|
"text/markdown": {
|
||||||
content: field.name,
|
content: field.name,
|
||||||
|
|
@ -332,7 +340,7 @@ export default apiRoute((app) =>
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
const parsedValue = await versiaTextToHtml(
|
const parsedValue = await contentToHtml(
|
||||||
new VersiaEntities.TextContentFormat({
|
new VersiaEntities.TextContentFormat({
|
||||||
"text/markdown": {
|
"text/markdown": {
|
||||||
content: field.value,
|
content: field.value,
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { config } from "@versia-server/config";
|
import { config } from "~/config";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(1);
|
const { users, deleteUsers } = await getTestUsers(1);
|
||||||
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { Account } from "@versia/client/schemas";
|
import { Account } from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { apiRoute, auth } from "@versia-server/kit/api";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { apiRoute, auth } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -2,13 +2,15 @@ import {
|
||||||
Application as ApplicationSchema,
|
Application as ApplicationSchema,
|
||||||
CredentialApplication as CredentialApplicationSchema,
|
CredentialApplication as CredentialApplicationSchema,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Application } from "@versia/kit/db";
|
||||||
import { apiRoute, handleZodError, jsonOrForm } from "@versia-server/kit/api";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { Client } from "@versia-server/kit/db";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, handleZodError, jsonOrForm } from "@/api";
|
||||||
import { randomString } from "@/math";
|
import { randomString } from "@/math";
|
||||||
import { rateLimit } from "../../../../middlewares/rate-limit.ts";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { rateLimit } from "~/middlewares/rate-limit";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -41,20 +43,22 @@ export default apiRoute((app) =>
|
||||||
z.object({
|
z.object({
|
||||||
client_name: ApplicationSchema.shape.name,
|
client_name: ApplicationSchema.shape.name,
|
||||||
redirect_uris: ApplicationSchema.shape.redirect_uris.or(
|
redirect_uris: ApplicationSchema.shape.redirect_uris.or(
|
||||||
ApplicationSchema.shape.redirect_uri,
|
ApplicationSchema.shape.redirect_uri.transform((u) =>
|
||||||
|
u.split("\n"),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
scopes: z.string().default("read").meta({
|
scopes: z
|
||||||
description: "Space separated list of scopes.",
|
.string()
|
||||||
type: "string",
|
.default("read")
|
||||||
}),
|
.transform((s) => s.split(" "))
|
||||||
|
.openapi({
|
||||||
|
description: "Space separated list of scopes.",
|
||||||
|
}),
|
||||||
// Allow empty websites because Traewelling decides to give an empty
|
// Allow empty websites because Traewelling decides to give an empty
|
||||||
// value instead of not providing anything at all
|
// value instead of not providing anything at all
|
||||||
website: ApplicationSchema.shape.website
|
website: ApplicationSchema.shape.website
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal(""))
|
.or(z.literal("").transform(() => undefined)),
|
||||||
.meta({
|
|
||||||
type: "string",
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
handleZodError,
|
handleZodError,
|
||||||
),
|
),
|
||||||
|
|
@ -62,14 +66,13 @@ export default apiRoute((app) =>
|
||||||
const { client_name, redirect_uris, scopes, website } =
|
const { client_name, redirect_uris, scopes, website } =
|
||||||
context.req.valid("json");
|
context.req.valid("json");
|
||||||
|
|
||||||
const app = await Client.insert({
|
const app = await Application.insert({
|
||||||
id: randomString(32, "base64url"),
|
id: randomUUIDv7(),
|
||||||
name: client_name,
|
name: client_name,
|
||||||
redirectUris: Array.isArray(redirect_uris)
|
redirectUri: redirect_uris.join("\n"),
|
||||||
? redirect_uris
|
scopes: scopes.join(" "),
|
||||||
: [redirect_uris],
|
website,
|
||||||
scopes: scopes.split(" "),
|
clientId: randomString(32, "base64url"),
|
||||||
website: website || undefined,
|
|
||||||
secret: randomString(64, "base64url"),
|
secret: randomString(64, "base64url"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2,10 +2,11 @@ import {
|
||||||
Application as ApplicationSchema,
|
Application as ApplicationSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Application } from "@versia/kit/db";
|
||||||
import { apiRoute, auth } from "@versia-server/kit/api";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { Client } from "@versia-server/kit/db";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { apiRoute, auth } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -38,7 +39,7 @@ export default apiRoute((app) =>
|
||||||
async (context) => {
|
async (context) => {
|
||||||
const { token } = context.get("auth");
|
const { token } = context.get("auth");
|
||||||
|
|
||||||
const application = await Client.getFromToken(
|
const application = await Application.getFromToken(
|
||||||
token.data.accessToken,
|
token.data.accessToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(3);
|
const { users, deleteUsers } = await getTestUsers(3);
|
||||||
|
|
||||||
|
|
@ -2,13 +2,14 @@ import {
|
||||||
Account as AccountSchema,
|
Account as AccountSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Timeline } from "@versia/kit/db";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { Timeline } from "@versia-server/kit/db";
|
|
||||||
import { Users } from "@versia-server/kit/tables";
|
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -32,7 +33,7 @@ export default apiRoute((app) =>
|
||||||
link: z
|
link: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.meta({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"Links to the next and previous pages",
|
"Links to the next and previous pages",
|
||||||
example:
|
example:
|
||||||
|
|
@ -55,24 +56,30 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
max_id: AccountSchema.shape.id.optional().meta({
|
max_id: AccountSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||||
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||||
}),
|
}),
|
||||||
since_id: AccountSchema.shape.id.optional().meta({
|
since_id: AccountSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
min_id: AccountSchema.shape.id.optional().meta({
|
min_id: AccountSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
limit: z.coerce.number().int().min(1).max(80).default(40).meta({
|
limit: z.coerce
|
||||||
description: "Maximum number of results to return.",
|
.number()
|
||||||
}),
|
.int()
|
||||||
|
.min(1)
|
||||||
|
.max(80)
|
||||||
|
.default(40)
|
||||||
|
.openapi({
|
||||||
|
description: "Maximum number of results to return.",
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
handleZodError,
|
handleZodError,
|
||||||
),
|
),
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { generateClient } from "@versia-server/tests";
|
import { generateClient } from "~/tests/utils";
|
||||||
|
|
||||||
// /api/v1/challenges
|
// /api/v1/challenges
|
||||||
describe("/api/v1/challenges", () => {
|
describe("/api/v1/challenges", () => {
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { Challenge } from "@versia/client/schemas";
|
import { Challenge } from "@versia/client/schemas";
|
||||||
import { config } from "@versia-server/config";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { apiRoute, auth } from "@versia-server/kit/api";
|
import { apiRoute, auth } from "@/api";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
|
||||||
import { generateChallenge } from "@/challenges";
|
import { generateChallenge } from "@/challenges";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { db } from "@versia-server/kit/db";
|
import { db } from "@versia/kit/db";
|
||||||
import { Emojis } from "@versia-server/kit/tables";
|
import { Emojis } from "@versia/kit/tables";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
|
||||||
import { inArray } from "drizzle-orm";
|
import { inArray } from "drizzle-orm";
|
||||||
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
|
|
||||||
|
|
@ -2,13 +2,14 @@ import {
|
||||||
CustomEmoji as CustomEmojiSchema,
|
CustomEmoji as CustomEmojiSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Emoji } from "@versia/kit/db";
|
||||||
import { apiRoute, auth } from "@versia-server/kit/api";
|
import { Emojis } from "@versia/kit/tables";
|
||||||
import { Emoji } from "@versia-server/kit/db";
|
|
||||||
import { Emojis } from "@versia-server/kit/tables";
|
|
||||||
import { and, eq, isNull, or } from "drizzle-orm";
|
import { and, eq, isNull, or } from "drizzle-orm";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { db } from "@versia-server/kit/db";
|
import { db } from "@versia/kit/db";
|
||||||
import { Emojis } from "@versia-server/kit/tables";
|
import { Emojis } from "@versia/kit/tables";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
|
||||||
import { inArray } from "drizzle-orm";
|
import { inArray } from "drizzle-orm";
|
||||||
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
let id = "";
|
let id = "";
|
||||||
|
|
@ -2,18 +2,19 @@ import {
|
||||||
CustomEmoji as CustomEmojiSchema,
|
CustomEmoji as CustomEmojiSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { config } from "@versia-server/config";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
apiRoute,
|
apiRoute,
|
||||||
auth,
|
auth,
|
||||||
handleZodError,
|
handleZodError,
|
||||||
jsonOrForm,
|
jsonOrForm,
|
||||||
withEmojiParam,
|
withEmojiParam,
|
||||||
} from "@versia-server/kit/api";
|
} from "@/api";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { mimeLookup } from "@/content_types";
|
import { mimeLookup } from "@/content_types";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -112,28 +113,29 @@ export default apiRoute((app) => {
|
||||||
"json",
|
"json",
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
shortcode: CustomEmojiSchema.shape.shortcode
|
shortcode: CustomEmojiSchema.shape.shortcode.max(
|
||||||
.max(config.validation.emojis.max_shortcode_characters)
|
config.validation.emojis.max_shortcode_characters,
|
||||||
.refine(
|
),
|
||||||
(s) =>
|
|
||||||
!config.validation.filters.emoji_shortcode.some(
|
|
||||||
(filter) => filter.test(s),
|
|
||||||
),
|
|
||||||
"Shortcode contains blocked words",
|
|
||||||
),
|
|
||||||
element: z
|
element: z
|
||||||
|
.string()
|
||||||
.url()
|
.url()
|
||||||
.meta({
|
.transform((a) => new URL(a))
|
||||||
|
.openapi({
|
||||||
description: "Emoji image URL",
|
description: "Emoji image URL",
|
||||||
})
|
})
|
||||||
.or(
|
.or(
|
||||||
z
|
z
|
||||||
.file()
|
.instanceof(File)
|
||||||
.max(config.validation.emojis.max_bytes)
|
.openapi({
|
||||||
.meta({
|
|
||||||
description:
|
description:
|
||||||
"Emoji image encoded using multipart/form-data",
|
"Emoji image encoded using multipart/form-data",
|
||||||
}),
|
})
|
||||||
|
.refine(
|
||||||
|
(v) =>
|
||||||
|
v.size <=
|
||||||
|
config.validation.emojis.max_bytes,
|
||||||
|
`Emoji must be less than ${config.validation.emojis.max_bytes} bytes`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
category: CustomEmojiSchema.shape.category.optional(),
|
category: CustomEmojiSchema.shape.category.optional(),
|
||||||
alt: CustomEmojiSchema.shape.description
|
alt: CustomEmojiSchema.shape.description
|
||||||
|
|
@ -187,7 +189,7 @@ export default apiRoute((app) => {
|
||||||
const contentType =
|
const contentType =
|
||||||
element instanceof File
|
element instanceof File
|
||||||
? element.type
|
? element.type
|
||||||
: await mimeLookup(new URL(element));
|
: await mimeLookup(element);
|
||||||
|
|
||||||
if (!contentType.startsWith("image/")) {
|
if (!contentType.startsWith("image/")) {
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
|
|
@ -200,7 +202,7 @@ export default apiRoute((app) => {
|
||||||
if (element instanceof File) {
|
if (element instanceof File) {
|
||||||
await emoji.media.updateFromFile(element);
|
await emoji.media.updateFromFile(element);
|
||||||
} else {
|
} else {
|
||||||
await emoji.media.updateFromUrl(new URL(element));
|
await emoji.media.updateFromUrl(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { db } from "@versia-server/kit/db";
|
import { db } from "@versia/kit/db";
|
||||||
import { Emojis } from "@versia-server/kit/tables";
|
import { Emojis } from "@versia/kit/tables";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
|
||||||
import { inArray } from "drizzle-orm";
|
import { inArray } from "drizzle-orm";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(3);
|
const { users, deleteUsers } = await getTestUsers(3);
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ const createImage = async (name: string): Promise<File> => {
|
||||||
.png()
|
.png()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
return new File([inputBuffer as BlobPart], name, {
|
return new File([inputBuffer], name, {
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -2,21 +2,17 @@ import {
|
||||||
CustomEmoji as CustomEmojiSchema,
|
CustomEmoji as CustomEmojiSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { config } from "@versia-server/config";
|
import { Emoji, Media } from "@versia/kit/db";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Emojis } from "@versia/kit/tables";
|
||||||
import {
|
|
||||||
apiRoute,
|
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
jsonOrForm,
|
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { Emoji, Media } from "@versia-server/kit/db";
|
|
||||||
import { Emojis } from "@versia-server/kit/tables";
|
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { and, eq, isNull, or } from "drizzle-orm";
|
import { and, eq, isNull, or } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError, jsonOrForm } from "@/api";
|
||||||
import { mimeLookup } from "@/content_types";
|
import { mimeLookup } from "@/content_types";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -49,25 +45,29 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"json",
|
"json",
|
||||||
z.object({
|
z.object({
|
||||||
shortcode: CustomEmojiSchema.shape.shortcode
|
shortcode: CustomEmojiSchema.shape.shortcode.max(
|
||||||
.max(config.validation.emojis.max_shortcode_characters)
|
config.validation.emojis.max_shortcode_characters,
|
||||||
.refine(
|
),
|
||||||
(s) =>
|
|
||||||
!config.validation.filters.emoji_shortcode.some(
|
|
||||||
(filter) => filter.test(s),
|
|
||||||
),
|
|
||||||
"Shortcode contains blocked words",
|
|
||||||
),
|
|
||||||
element: z
|
element: z
|
||||||
|
.string()
|
||||||
.url()
|
.url()
|
||||||
.meta({
|
.transform((a) => new URL(a))
|
||||||
|
.openapi({
|
||||||
description: "Emoji image URL",
|
description: "Emoji image URL",
|
||||||
})
|
})
|
||||||
.or(
|
.or(
|
||||||
z.file().max(config.validation.emojis.max_bytes).meta({
|
z
|
||||||
description:
|
.instanceof(File)
|
||||||
"Emoji image encoded using multipart/form-data",
|
.openapi({
|
||||||
}),
|
description:
|
||||||
|
"Emoji image encoded using multipart/form-data",
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(v) =>
|
||||||
|
v.size <=
|
||||||
|
config.validation.emojis.max_bytes,
|
||||||
|
`Emoji must be less than ${config.validation.emojis.max_bytes} bytes`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
category: CustomEmojiSchema.shape.category.optional(),
|
category: CustomEmojiSchema.shape.category.optional(),
|
||||||
alt: CustomEmojiSchema.shape.description
|
alt: CustomEmojiSchema.shape.description
|
||||||
|
|
@ -112,7 +112,7 @@ export default apiRoute((app) =>
|
||||||
const contentType =
|
const contentType =
|
||||||
element instanceof File
|
element instanceof File
|
||||||
? element.type
|
? element.type
|
||||||
: await mimeLookup(new URL(element));
|
: await mimeLookup(element);
|
||||||
|
|
||||||
if (!contentType.startsWith("image/")) {
|
if (!contentType.startsWith("image/")) {
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
|
|
@ -127,7 +127,7 @@ export default apiRoute((app) =>
|
||||||
? await Media.fromFile(element, {
|
? await Media.fromFile(element, {
|
||||||
description: alt ?? undefined,
|
description: alt ?? undefined,
|
||||||
})
|
})
|
||||||
: await Media.fromUrl(new URL(element), {
|
: await Media.fromUrl(element, {
|
||||||
description: alt ?? undefined,
|
description: alt ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { RolePermission, Status as StatusSchema } from "@versia/client/schemas";
|
import { RolePermission, Status as StatusSchema } from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Timeline } from "@versia/kit/db";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { Notes } from "@versia/kit/tables";
|
||||||
import { Timeline } from "@versia-server/kit/db";
|
|
||||||
import { Notes } from "@versia-server/kit/tables";
|
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -29,7 +30,7 @@ export default apiRoute((app) =>
|
||||||
link: z
|
link: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.meta({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"Links to the next and previous pages",
|
"Links to the next and previous pages",
|
||||||
example:
|
example:
|
||||||
|
|
@ -51,24 +52,30 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
max_id: StatusSchema.shape.id.optional().meta({
|
max_id: StatusSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||||
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||||
}),
|
}),
|
||||||
since_id: StatusSchema.shape.id.optional().meta({
|
since_id: StatusSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
min_id: StatusSchema.shape.id.optional().meta({
|
min_id: StatusSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
limit: z.coerce.number().int().min(1).max(80).default(40).meta({
|
limit: z.coerce
|
||||||
description: "Maximum number of results to return.",
|
.number()
|
||||||
}),
|
.int()
|
||||||
|
.min(1)
|
||||||
|
.max(80)
|
||||||
|
.default(40)
|
||||||
|
.openapi({
|
||||||
|
description: "Maximum number of results to return.",
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
handleZodError,
|
handleZodError,
|
||||||
),
|
),
|
||||||
|
|
@ -3,11 +3,12 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Relationship, User } from "@versia/kit/db";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { Relationship, User } from "@versia-server/kit/db";
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -3,11 +3,12 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Relationship, User } from "@versia/kit/db";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { Relationship, User } from "@versia-server/kit/db";
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -2,13 +2,14 @@ import {
|
||||||
Account as AccountSchema,
|
Account as AccountSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { Timeline } from "@versia/kit/db";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { Timeline } from "@versia-server/kit/db";
|
|
||||||
import { Users } from "@versia-server/kit/tables";
|
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute, auth, handleZodError } from "@/api";
|
||||||
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -34,7 +35,7 @@ export default apiRoute((app) =>
|
||||||
link: z
|
link: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.meta({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"Links to the next and previous pages",
|
"Links to the next and previous pages",
|
||||||
example:
|
example:
|
||||||
|
|
@ -56,24 +57,30 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
max_id: AccountSchema.shape.id.optional().meta({
|
max_id: AccountSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||||
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||||
}),
|
}),
|
||||||
since_id: AccountSchema.shape.id.optional().meta({
|
since_id: AccountSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
min_id: AccountSchema.shape.id.optional().meta({
|
min_id: AccountSchema.shape.id.optional().openapi({
|
||||||
description:
|
description:
|
||||||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
limit: z.coerce.number().int().min(1).max(80).default(40).meta({
|
limit: z.coerce
|
||||||
description: "Maximum number of results to return.",
|
.number()
|
||||||
}),
|
.int()
|
||||||
|
.min(1)
|
||||||
|
.max(80)
|
||||||
|
.default(40)
|
||||||
|
.openapi({
|
||||||
|
description: "Maximum number of results to return.",
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
handleZodError,
|
handleZodError,
|
||||||
),
|
),
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { config } from "@versia-server/config";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { apiRoute } from "@versia-server/kit/api";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute } from "@/api";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { generateClient } from "@versia-server/tests";
|
import { generateClient } from "~/tests/utils";
|
||||||
|
|
||||||
// /api/v1/instance/extended_description
|
// /api/v1/instance/extended_description
|
||||||
describe("/api/v1/instance/extended_description", () => {
|
describe("/api/v1/instance/extended_description", () => {
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { ExtendedDescription as ExtendedDescriptionSchema } from "@versia/client/schemas";
|
import { ExtendedDescription as ExtendedDescriptionSchema } from "@versia/client/schemas";
|
||||||
import { config } from "@versia-server/config";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { apiRoute } from "@versia-server/kit/api";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { markdownToHtml } from "@versia-server/kit/markdown";
|
import { apiRoute } from "@/api";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { markdownParse } from "~/classes/functions/status";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -26,7 +27,7 @@ export default apiRoute((app) =>
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (context) => {
|
async (context) => {
|
||||||
const content = await markdownToHtml(
|
const content = await markdownParse(
|
||||||
config.instance.extended_description_path?.content ??
|
config.instance.extended_description_path?.content ??
|
||||||
"This is a [Versia](https://versia.pub) server with the default extended description.",
|
"This is a [Versia](https://versia.pub) server with the default extended description.",
|
||||||
);
|
);
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { InstanceV1 as InstanceV1Schema } from "@versia/client/schemas";
|
import { InstanceV1 as InstanceV1Schema } from "@versia/client/schemas";
|
||||||
import { config } from "@versia-server/config";
|
import { Instance, Note, User } from "@versia/kit/db";
|
||||||
import { apiRoute } from "@versia-server/kit/api";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { Instance, Note, User } from "@versia-server/kit/db";
|
|
||||||
import { markdownToHtml } from "@versia-server/kit/markdown";
|
|
||||||
import { Users } from "@versia-server/kit/tables";
|
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { resolver } from "hono-openapi/zod";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import manifest from "../../../../../../package.json" with { type: "json" };
|
import { apiRoute } from "@/api";
|
||||||
|
import { markdownParse } from "~/classes/functions/status";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
import manifest from "~/package.json";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -48,7 +49,18 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
const knownDomainsCount = await Instance.getCount();
|
const knownDomainsCount = await Instance.getCount();
|
||||||
|
|
||||||
const content = await markdownToHtml(
|
const oidcConfig = config.plugins?.config?.["@versia/openid"] as
|
||||||
|
| {
|
||||||
|
forced?: boolean;
|
||||||
|
providers?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const content = await markdownParse(
|
||||||
config.instance.extended_description_path?.content ??
|
config.instance.extended_description_path?.content ??
|
||||||
"This is a [Versia](https://versia.pub) server with the default extended description.",
|
"This is a [Versia](https://versia.pub) server with the default extended description.",
|
||||||
);
|
);
|
||||||
|
|
@ -110,14 +122,15 @@ export default apiRoute((app) =>
|
||||||
},
|
},
|
||||||
version: "4.3.0-alpha.3+glitch",
|
version: "4.3.0-alpha.3+glitch",
|
||||||
versia_version: version,
|
versia_version: version,
|
||||||
|
// TODO: Put into plugin directly
|
||||||
sso: {
|
sso: {
|
||||||
providers: config.authentication.openid_providers.map(
|
forced: oidcConfig?.forced ?? false,
|
||||||
(p) => ({
|
providers:
|
||||||
|
oidcConfig?.providers?.map((p) => ({
|
||||||
name: p.name,
|
name: p.name,
|
||||||
icon: p.icon?.href,
|
icon: p.icon,
|
||||||
id: p.id,
|
id: p.id,
|
||||||
}),
|
})) ?? [],
|
||||||
),
|
|
||||||
},
|
},
|
||||||
contact_account: (contactAccount as User)?.toApi(),
|
contact_account: (contactAccount as User)?.toApi(),
|
||||||
} satisfies z.infer<typeof InstanceV1Schema>);
|
} satisfies z.infer<typeof InstanceV1Schema>);
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { generateClient } from "@versia-server/tests";
|
import { generateClient } from "~/tests/utils";
|
||||||
|
|
||||||
// /api/v1/instance/privacy_policy
|
// /api/v1/instance/privacy_policy
|
||||||
describe("/api/v1/instance/privacy_policy", () => {
|
describe("/api/v1/instance/privacy_policy", () => {
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { PrivacyPolicy as PrivacyPolicySchema } from "@versia/client/schemas";
|
import { PrivacyPolicy as PrivacyPolicySchema } from "@versia/client/schemas";
|
||||||
import { config } from "@versia-server/config";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { apiRoute } from "@versia-server/kit/api";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { markdownToHtml } from "@versia-server/kit/markdown";
|
import { apiRoute } from "@/api";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { markdownParse } from "~/classes/functions/status";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -26,7 +27,7 @@ export default apiRoute((app) =>
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (context) => {
|
async (context) => {
|
||||||
const content = await markdownToHtml(
|
const content = await markdownParse(
|
||||||
config.instance.privacy_policy_path?.content ??
|
config.instance.privacy_policy_path?.content ??
|
||||||
"This instance has not provided any privacy policy.",
|
"This instance has not provided any privacy policy.",
|
||||||
);
|
);
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { config } from "@versia-server/config";
|
import { config } from "~/config.ts";
|
||||||
import { generateClient } from "@versia-server/tests";
|
import { generateClient } from "~/tests/utils";
|
||||||
|
|
||||||
// /api/v1/instance/rules
|
// /api/v1/instance/rules
|
||||||
describe("/api/v1/instance/rules", () => {
|
describe("/api/v1/instance/rules", () => {
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { Rule as RuleSchema } from "@versia/client/schemas";
|
import { Rule as RuleSchema } from "@versia/client/schemas";
|
||||||
import { config } from "@versia-server/config";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { apiRoute } from "@versia-server/kit/api";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiRoute } from "@/api";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { generateClient } from "@versia-server/tests";
|
import { generateClient } from "~/tests/utils";
|
||||||
|
|
||||||
// /api/v1/instance/terms_of_service
|
// /api/v1/instance/terms_of_service
|
||||||
describe("/api/v1/instance/terms_of_service", () => {
|
describe("/api/v1/instance/terms_of_service", () => {
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { TermsOfService as TermsOfServiceSchema } from "@versia/client/schemas";
|
import { TermsOfService as TermsOfServiceSchema } from "@versia/client/schemas";
|
||||||
import { config } from "@versia-server/config";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { apiRoute } from "@versia-server/kit/api";
|
import { resolver } from "hono-openapi/zod";
|
||||||
import { markdownToHtml } from "@versia-server/kit/markdown";
|
import { apiRoute } from "@/api";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { markdownParse } from "~/classes/functions/status";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -27,7 +28,7 @@ export default apiRoute((app) =>
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (context) => {
|
async (context) => {
|
||||||
const content = await markdownToHtml(
|
const content = await markdownParse(
|
||||||
config.instance.tos_path?.content ??
|
config.instance.tos_path?.content ??
|
||||||
"This instance has not provided any terms of service.",
|
"This instance has not provided any terms of service.",
|
||||||
);
|
);
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import {
|
import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
|
||||||
generateClient,
|
|
||||||
getTestStatuses,
|
|
||||||
getTestUsers,
|
|
||||||
} from "@versia-server/tests";
|
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(1);
|
const { users, deleteUsers } = await getTestUsers(1);
|
||||||
const timeline = await getTestStatuses(10, users[0]);
|
const timeline = await getTestStatuses(10, users[0]);
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue