mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
Compare commits
16 commits
c93071666a
...
5e84fb66f9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e84fb66f9 | ||
|
|
1430d6f7e7 | ||
|
|
f00ac1a590 | ||
|
|
f260064083 | ||
|
|
f2e9c862a6 | ||
|
|
82bb92768c | ||
|
|
c63b2b320b | ||
|
|
a9dbd2cc4e | ||
|
|
ae207c10b6 | ||
|
|
955a933fe9 | ||
|
|
45c3f6ae3f | ||
|
|
bfa7a06958 | ||
|
|
1a0a27bee1 | ||
|
|
6f97903f3b | ||
|
|
1bfc5fb013 | ||
|
|
4eae4cd062 |
|
|
@ -1,16 +1,18 @@
|
||||||
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"]
|
||||||
|
|
|
||||||
13
.github/config.workflow.toml
vendored
13
.github/config.workflow.toml
vendored
|
|
@ -453,17 +453,8 @@ log_level = "info" # For console output
|
||||||
# log_level = "info"
|
# log_level = "info"
|
||||||
|
|
||||||
[authentication]
|
[authentication]
|
||||||
# If enabled, Versia will require users to log in with an OpenID provider
|
# Run Versia Server with this value missing to generate a new key
|
||||||
forced_openid = false
|
key = "ZWcwanRaQAqY3ChUro/Jey9XGQjzsxEed5iqTp4yFr8W6vEnXdz91F/Pu/uf7HBMbNeIK7V6aHsM0lq9onrO8Q=="
|
||||||
|
|
||||||
# Allow registration with OpenID providers
|
|
||||||
# If signups.registration is false, it will only be possible to register with OpenID
|
|
||||||
openid_registration = true
|
|
||||||
|
|
||||||
[authentication.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
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
- [x] 🚀 Upgraded Bun to `1.2.18`
|
- [x] 🚀 Upgraded Bun to `1.3.2`
|
||||||
|
|
||||||
# `0.8.0` • Federation 2: Electric Boogaloo
|
# `0.8.0` • Federation 2: Electric Boogaloo
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ RUN bun run build api
|
||||||
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.2.18-alpine
|
FROM oven/bun:1.3.2-alpine
|
||||||
|
|
||||||
# Install libstdc++ for Bun and create app directory
|
# Install libstdc++ for Bun and create app directory
|
||||||
RUN mkdir -p /app
|
RUN mkdir -p /app
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ 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.2.18-alpine
|
FROM oven/bun:1.3.2-alpine
|
||||||
|
|
||||||
# Install libstdc++ for Bun and create app directory
|
# Install libstdc++ for Bun and create app directory
|
||||||
RUN mkdir -p /app
|
RUN mkdir -p /app
|
||||||
|
|
|
||||||
2
api.ts
2
api.ts
|
|
@ -2,7 +2,7 @@ import process from "node:process";
|
||||||
import { appFactory } from "@versia-server/api";
|
import { appFactory } from "@versia-server/api";
|
||||||
import { config } from "@versia-server/config";
|
import { config } from "@versia-server/config";
|
||||||
import { Youch } from "youch";
|
import { Youch } from "youch";
|
||||||
import { createServer } from "@/server";
|
import { createServer } from "@/server.ts";
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
process.exit();
|
process.exit();
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
getTestUsers,
|
getTestUsers,
|
||||||
} from "@versia-server/tests";
|
} from "@versia-server/tests";
|
||||||
import { bench, run } from "mitata";
|
import { bench, run } from "mitata";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod";
|
||||||
|
|
||||||
const { users, tokens, deleteUsers } = await getTestUsers(5);
|
const { users, tokens, deleteUsers } = await getTestUsers(5);
|
||||||
await getTestStatuses(40, users[0]);
|
await getTestStatuses(40, users[0]);
|
||||||
|
|
|
||||||
18
biome.json
18
biome.json
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.3.4/schema.json",
|
||||||
"assist": {
|
"assist": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"source": {
|
"source": {
|
||||||
|
|
@ -119,23 +119,11 @@
|
||||||
"noUnusedPrivateClassMembers": "error"
|
"noUnusedPrivateClassMembers": "error"
|
||||||
},
|
},
|
||||||
"nursery": {
|
"nursery": {
|
||||||
"noBitwiseOperators": "error",
|
"noFloatingPromises": "error"
|
||||||
"noConstantBinaryExpression": "error",
|
|
||||||
"noFloatingPromises": "error",
|
|
||||||
"noGlobalDirnameFilename": "error",
|
|
||||||
"noProcessGlobal": "warn",
|
|
||||||
"noTsIgnore": "warn",
|
|
||||||
"useReadonlyClassProperties": "error",
|
|
||||||
"useConsistentObjectDefinition": {
|
|
||||||
"level": "warn",
|
|
||||||
"options": {
|
|
||||||
"syntax": "shorthand"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"useParseIntRadix": "warn"
|
|
||||||
},
|
},
|
||||||
"complexity": {
|
"complexity": {
|
||||||
"noForEach": "error",
|
"noForEach": "error",
|
||||||
|
"noImportantStyles": "off",
|
||||||
"noUselessStringConcat": "error",
|
"noUselessStringConcat": "error",
|
||||||
"useDateNow": "error",
|
"useDateNow": "error",
|
||||||
"noUselessStringRaw": "warn",
|
"noUselessStringRaw": "warn",
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,6 @@
|
||||||
|
|
||||||
[test]
|
[test]
|
||||||
preload = ["./packages/tests/setup.ts"]
|
preload = ["./packages/tests/setup.ts"]
|
||||||
|
|
||||||
|
[install]
|
||||||
|
linker = "hoisted"
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ describe("BlurhashPreprocessor", () => {
|
||||||
.png()
|
.png()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
const inputFile = new File([inputBuffer], "test.png", {
|
const inputFile = new File([inputBuffer as BlobPart], "test.png", {
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
});
|
});
|
||||||
const result = await calculateBlurhash(inputFile);
|
const result = await calculateBlurhash(inputFile);
|
||||||
|
|
@ -46,7 +46,7 @@ describe("BlurhashPreprocessor", () => {
|
||||||
.png()
|
.png()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
const inputFile = new File([inputBuffer], "test.png", {
|
const inputFile = new File([inputBuffer as BlobPart], "test.png", {
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ describe("ImageConversionPreprocessor", () => {
|
||||||
.jpeg()
|
.jpeg()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
const inputFile = new File([inputBuffer], "test.jpg", {
|
const inputFile = new File([inputBuffer as BlobPart], "test.jpg", {
|
||||||
type: "image/jpeg",
|
type: "image/jpeg",
|
||||||
});
|
});
|
||||||
const result = await convertImage(inputFile, "image/webp");
|
const result = await convertImage(inputFile, "image/webp");
|
||||||
|
|
@ -74,7 +74,7 @@ describe("ImageConversionPreprocessor", () => {
|
||||||
.png()
|
.png()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
const inputFile = new File([inputBuffer], "test.png", {
|
const inputFile = new File([inputBuffer as BlobPart], "test.png", {
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -96,7 +96,7 @@ describe("ImageConversionPreprocessor", () => {
|
||||||
.gif()
|
.gif()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
const inputFile = new File([inputBuffer], "animated.gif", {
|
const inputFile = new File([inputBuffer as BlobPart], "animated.gif", {
|
||||||
type: "image/gif",
|
type: "image/gif",
|
||||||
});
|
});
|
||||||
const result = await convertImage(inputFile, "image/webp");
|
const result = await convertImage(inputFile, "image/webp");
|
||||||
|
|
@ -122,7 +122,7 @@ describe("ImageConversionPreprocessor", () => {
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
const inputFile = new File(
|
const inputFile = new File(
|
||||||
[inputBuffer],
|
[inputBuffer as BlobPart],
|
||||||
"test image with spaces.png",
|
"test image with spaces.png",
|
||||||
{ type: "image/png" },
|
{ type: "image/png" },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ export const convertImage = async (
|
||||||
const convertedBuffer = await sharpCommand[commandName]().toBuffer();
|
const convertedBuffer = await sharpCommand[commandName]().toBuffer();
|
||||||
|
|
||||||
return new File(
|
return new File(
|
||||||
[convertedBuffer],
|
[convertedBuffer as BlobPart],
|
||||||
getReplacedFileName(file.name, commandName),
|
getReplacedFileName(file.name, commandName),
|
||||||
{
|
{
|
||||||
type: targetFormat,
|
type: targetFormat,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Token } from "@versia-server/kit/db";
|
import { Client, Token } from "@versia-server/kit/db";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
||||||
|
|
@ -22,13 +22,24 @@ export const generateTokenCommand = defineCommand(
|
||||||
throw new Error(`User ${chalk.gray(username)} not found.`);
|
throw new Error(`User ${chalk.gray(username)} not found.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const application = await Client.insert({
|
||||||
|
id:
|
||||||
|
user.id +
|
||||||
|
Buffer.from(
|
||||||
|
crypto.getRandomValues(new Uint8Array(32)),
|
||||||
|
).toString("base64"),
|
||||||
|
name: "Versia",
|
||||||
|
redirectUris: [],
|
||||||
|
scopes: ["openid", "profile", "email"],
|
||||||
|
secret: "",
|
||||||
|
});
|
||||||
|
|
||||||
const token = await Token.insert({
|
const token = await Token.insert({
|
||||||
id: randomUUIDv7(),
|
id: randomUUIDv7(),
|
||||||
accessToken: randomString(64, "base64url"),
|
accessToken: randomString(64, "base64url"),
|
||||||
code: null,
|
scopes: ["read", "write", "follow"],
|
||||||
scope: "read write follow",
|
|
||||||
tokenType: "Bearer",
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
clientId: application.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.info(
|
console.info(
|
||||||
|
|
|
||||||
|
|
@ -459,17 +459,8 @@ log_level = "info" # For console output
|
||||||
# log_level = "info"
|
# log_level = "info"
|
||||||
|
|
||||||
[authentication]
|
[authentication]
|
||||||
# If enabled, Versia will require users to log in with an OpenID provider
|
# Run Versia Server with this value missing to generate a new key
|
||||||
forced_openid = false
|
# key = ""
|
||||||
|
|
||||||
# Allow registration with OpenID providers
|
|
||||||
# If signups.registration is false, it will only be possible to register with OpenID
|
|
||||||
openid_registration = true
|
|
||||||
|
|
||||||
# [authentication.keys]
|
|
||||||
# Run Versia Server with those values missing to generate a new key
|
|
||||||
# public = ""
|
|
||||||
# private = ""
|
|
||||||
|
|
||||||
# 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
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Multiple API routes are exposed for authentication, to be used by frontend developers.
|
Multiple API routes are exposed for authentication, to be used by frontend developers.
|
||||||
|
|
||||||
> [!INFO]
|
> [!INFO]
|
||||||
>
|
>
|
||||||
> These are different from the Client API routes, which are used by clients to interact with the Mastodon API.
|
> These are different from the Client API routes, which are used by clients to interact with the Mastodon API.
|
||||||
|
|
||||||
A frontend is a web application that is designed to be the primary user interface for an instance. It is used also used by clients to perform authentication.
|
A frontend is a web application that is designed to be the primary user interface for an instance. It is used also used by clients to perform authentication.
|
||||||
|
|
@ -48,58 +48,6 @@ Frontend configuration.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Sign In
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/auth/login
|
|
||||||
```
|
|
||||||
|
|
||||||
Allows users to sign in to the instance. This is the first step in the authentication process.
|
|
||||||
|
|
||||||
- **Returns**: `302 Found` with a `Location` header to redirect the user to the next step, as well as a `Set-Cookie` header with the session JWT.
|
|
||||||
- **Authentication**: Not required
|
|
||||||
- **Permissions**: None
|
|
||||||
- **Version History**:
|
|
||||||
- `0.7.0`: First documented.
|
|
||||||
|
|
||||||
### Request
|
|
||||||
|
|
||||||
- `identifier` (string, required): The username or email of the user. Case-insensitive.
|
|
||||||
- `password` (string, required): The password of the user.
|
|
||||||
|
|
||||||
#### Query Parameters
|
|
||||||
|
|
||||||
- `client_id` (string, required): Client ID of the [application](https://docs.joinmastodon.org/entities/Application/) that is making the request.
|
|
||||||
- `redirect_uri` (string, required): Redirect URI of the [application](https://docs.joinmastodon.org/entities/Application/) that is making the request. Must match the saved value.
|
|
||||||
- `response_type` (string, required): Must be `code`.
|
|
||||||
- `scope` (string, required): OAuth2 scopes. Must match the value indicated in the [application](https://docs.joinmastodon.org/entities/Application/).
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/auth/login?client_id=123&redirect_uri=https%3A%2F%2Fexample.com%2Fauth&response_type=code&scope=read%20write
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"identifier": "bobjones@gmail.com",
|
|
||||||
"password": "hunter2"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Response
|
|
||||||
|
|
||||||
#### `302 Found`
|
|
||||||
|
|
||||||
Redirects the user to the consent page with some query parameters. The frontend should redirect the user to this URL.
|
|
||||||
|
|
||||||
This response also has a `Set-Cookie` header with a [JSON Web Token](https://jwt.io/) that contains the user's session information. This JWT is signed with the instance's secret key, and must be included in all subsequent authentication requests.
|
|
||||||
|
|
||||||
```http
|
|
||||||
HTTP/2.0 302 Found
|
|
||||||
Location: /oauth/consent?client_id=123&redirect_uri=https%3A%2F%2Fexample.com%2Fauth&response_type=code&scope=read%20write
|
|
||||||
Set-Cookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=3600
|
|
||||||
```
|
|
||||||
|
|
||||||
## SSO Sign In
|
## SSO Sign In
|
||||||
|
|
||||||
```http
|
```http
|
||||||
|
|
@ -136,4 +84,4 @@ Redirects the user to the OpenID Connect provider's login page.
|
||||||
```http
|
```http
|
||||||
HTTP/2.0 302 Found
|
HTTP/2.0 302 Found
|
||||||
Location: https://accounts.google.com/o/oauth2/auth?client_id=123&redirect_uri=https%3A%2F%2Fexample.com%2Fauth&response_type=code&scope=openid%20email&state=123
|
Location: https://accounts.google.com/o/oauth2/auth?client_id=123&redirect_uri=https%3A%2F%2Fexample.com%2Fauth&response_type=code&scope=openid%20email&state=123
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ GET /oauth/authorize
|
||||||
|
|
||||||
This route should display a login form for the user to enter their username and password, as well as a list of OpenID providers to use if available.
|
This route should display a login form for the user to enter their username and password, as well as a list of OpenID providers to use if available.
|
||||||
|
|
||||||
The form should submit to [`POST /api/auth/login`](./auth.md#sign-in), or to the OpenID Connect flow.
|
The form should submit to the OpenID Connect flow.
|
||||||
|
|
||||||
Configurable in the Versia Server configuration at `frontend.routes.login`.
|
Configurable in the Versia Server configuration at `frontend.routes.login`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import type { Config } from "drizzle-kit";
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
dialect: "postgresql",
|
dialect: "postgresql",
|
||||||
out: "./drizzle/migrations",
|
out: "./packages/kit/tables/migrations",
|
||||||
schema: "./drizzle/schema.ts",
|
schema: "./packages/kit/tables/schema.ts",
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
/* host: "localhost",
|
/* host: "localhost",
|
||||||
port: 40000,
|
port: 40000,
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1751637120,
|
"lastModified": 1763421233,
|
||||||
"narHash": "sha256-xVNy/XopSfIG9c46nRmPaKfH1Gn/56vQ8++xWA8itO4=",
|
"narHash": "sha256-Stk9ZYRkGrnnpyJ4eqt9eQtdFWRRIvMxpNRf4sIegnw=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "5c724ed1388e53cc231ed98330a60eb2f7be4be3",
|
"rev": "89c2b2330e733d6cdb5eae7b899326930c2c0648",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ in
|
||||||
# Required else we get errors that our fixed-output derivation references store paths
|
# Required else we get errors that our fixed-output derivation references store paths
|
||||||
dontFixup = true;
|
dontFixup = true;
|
||||||
|
|
||||||
outputHash = "sha256-aG54v3luuJTmb/eonoILv3KBKW6mulk3xOpxLA6V5L8=";
|
outputHash = "sha256-gr4R+S4OusBtQtlskzjS+FEtT2mKCXcr6jk7EInXXMo=";
|
||||||
outputHashAlgo = "sha256";
|
outputHashAlgo = "sha256";
|
||||||
outputHashMode = "recursive";
|
outputHashMode = "recursive";
|
||||||
};
|
};
|
||||||
|
|
|
||||||
189
package.json
189
package.json
|
|
@ -20,100 +20,6 @@
|
||||||
"activitypub",
|
"activitypub",
|
||||||
"bun"
|
"bun"
|
||||||
],
|
],
|
||||||
"workspaces": {
|
|
||||||
"packages": [
|
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"catalog": {
|
|
||||||
"@biomejs/biome": "^2.0.6",
|
|
||||||
"@types/bun": "^1.2.18",
|
|
||||||
"@types/html-to-text": "^9.0.4",
|
|
||||||
"@types/markdown-it-container": "^2.0.10",
|
|
||||||
"@types/mime-types": "^3.0.1",
|
|
||||||
"@types/qs": "^6.14.0",
|
|
||||||
"@types/web-push": "^3.6.4",
|
|
||||||
"bun-bagel": "^1.2.0",
|
|
||||||
"drizzle-kit": "^0.31.4",
|
|
||||||
"mitt": "^3.0.1",
|
|
||||||
"markdown-it-image-figures": "^2.1.1",
|
|
||||||
"ts-prune": "^0.10.3",
|
|
||||||
"typescript": "^5.8.3",
|
|
||||||
"vitepress": "^1.6.3",
|
|
||||||
"vitepress-plugin-tabs": "^0.7.1",
|
|
||||||
"vitepress-sidebar": "^1.32.1",
|
|
||||||
"vue": "^3.5.17",
|
|
||||||
"@bull-board/api": "^6.11.0",
|
|
||||||
"@bull-board/hono": "^6.11.0",
|
|
||||||
"@clerc/plugin-completions": "^0.44.0",
|
|
||||||
"@clerc/plugin-friendly-error": "^0.44.0",
|
|
||||||
"@clerc/plugin-help": "^0.44.0",
|
|
||||||
"@clerc/plugin-not-found": "^0.44.0",
|
|
||||||
"@clerc/plugin-version": "^0.44.0",
|
|
||||||
"@hackmd/markdown-it-task-lists": "^2.1.4",
|
|
||||||
"@hono/standard-validator": "^0.1.2",
|
|
||||||
"@inquirer/confirm": "^5.1.13",
|
|
||||||
"@logtape/file": "^1.0.0",
|
|
||||||
"@logtape/logtape": "^1.0.0",
|
|
||||||
"@logtape/sentry": "^1.0.0",
|
|
||||||
"@logtape/otel": "^1.0.0",
|
|
||||||
"@scalar/hono-api-reference": "^0.9.7",
|
|
||||||
"@sentry/bun": "^9.35.0",
|
|
||||||
"altcha-lib": "^1.3.0",
|
|
||||||
"blurhash": "^2.0.5",
|
|
||||||
"bullmq": "^5.56.1",
|
|
||||||
"chalk": "^5.4.1",
|
|
||||||
"clerc": "^0.44.0",
|
|
||||||
"confbox": "^0.2.2",
|
|
||||||
"drizzle-orm": "^0.44.2",
|
|
||||||
"feed": "^5.1.0",
|
|
||||||
"hono": "^4.8.4",
|
|
||||||
"hono-openapi": "npm:@cpluspatch/hono-openapi@0.5.1",
|
|
||||||
"hono-rate-limiter": "^0.4.2",
|
|
||||||
"html-to-text": "^9.0.5",
|
|
||||||
"ioredis": "^5.6.1",
|
|
||||||
"ip-matching": "^2.1.2",
|
|
||||||
"iso-639-1": "^3.1.5",
|
|
||||||
"jose": "^6.0.11",
|
|
||||||
"linkify-html": "^4.3.1",
|
|
||||||
"linkify-string": "^4.3.1",
|
|
||||||
"linkifyjs": "^4.3.1",
|
|
||||||
"magic-regexp": "^0.10.0",
|
|
||||||
"markdown-it": "^14.1.0",
|
|
||||||
"markdown-it-anchor": "^9.2.0",
|
|
||||||
"markdown-it-container": "^4.0.0",
|
|
||||||
"markdown-it-mathjax3": "^4.3.2",
|
|
||||||
"markdown-it-toc-done-right": "^4.2.0",
|
|
||||||
"mime-types": "^3.0.1",
|
|
||||||
"mitata": "^1.0.34",
|
|
||||||
"oauth4webapi": "^3.5.5",
|
|
||||||
"ora": "^8.2.0",
|
|
||||||
"qs": "^6.14.0",
|
|
||||||
"sharp": "^0.34.2",
|
|
||||||
"sonic-channel": "^1.3.1",
|
|
||||||
"string-comparison": "^1.3.0",
|
|
||||||
"stringify-entities": "^4.0.4",
|
|
||||||
"unicode-emoji-json": "^0.8.0",
|
|
||||||
"uqr": "^0.1.2",
|
|
||||||
"web-push": "^3.6.7",
|
|
||||||
"xss": "^1.0.15",
|
|
||||||
"youch": "^4.1.0-beta.7",
|
|
||||||
"zod": "^3.25.74",
|
|
||||||
"zod-openapi": "^5.0.0",
|
|
||||||
"zod-validation-error": "^4.0.0-beta.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"maintainers": [
|
|
||||||
{
|
|
||||||
"email": "contact@cpluspatch.com",
|
|
||||||
"name": "Jesse Wierzbinski",
|
|
||||||
"url": "https://cpluspatch.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/versia-pub/server.git"
|
|
||||||
},
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
"cloc": "cloc . --exclude-dir node_modules,dist,.output,.nuxt,meta,logs --exclude-ext sql,log,pem",
|
"cloc": "cloc . --exclude-dir node_modules,dist,.output,.nuxt,meta,logs --exclude-ext sql,log,pem",
|
||||||
|
|
@ -129,6 +35,99 @@
|
||||||
"dev": "bun run --hot api.ts",
|
"dev": "bun run --hot api.ts",
|
||||||
"worker:dev": "bun run --hot worker.ts"
|
"worker:dev": "bun run --hot worker.ts"
|
||||||
},
|
},
|
||||||
|
"workspaces": {
|
||||||
|
"packages": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"catalog": {
|
||||||
|
"@biomejs/biome": "2.3.4",
|
||||||
|
"@types/bun": "~1.3.2",
|
||||||
|
"@types/html-to-text": "~9.0.4",
|
||||||
|
"@types/markdown-it-container": "~2.0.10",
|
||||||
|
"@types/mime-types": "~3.0.1",
|
||||||
|
"@types/qs": "~6.14.0",
|
||||||
|
"@types/web-push": "~3.6.4",
|
||||||
|
"bun-bagel": "~1.2.0",
|
||||||
|
"drizzle-kit": "~0.31.7",
|
||||||
|
"mitt": "~3.0.1",
|
||||||
|
"markdown-it-image-figures": "~2.1.1",
|
||||||
|
"ts-prune": "~0.10.3",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"vitepress": "~1.6.4",
|
||||||
|
"vitepress-plugin-tabs": "~0.7.3",
|
||||||
|
"vitepress-sidebar": "~1.33.0",
|
||||||
|
"vue": "~3.5.24",
|
||||||
|
"@bull-board/api": "~6.14.2",
|
||||||
|
"@bull-board/hono": "~6.14.2",
|
||||||
|
"@clerc/plugin-completions": "~0.44.0",
|
||||||
|
"@clerc/plugin-friendly-error": "~0.44.0",
|
||||||
|
"@clerc/plugin-help": "~0.44.0",
|
||||||
|
"@clerc/plugin-not-found": "~0.44.0",
|
||||||
|
"@clerc/plugin-version": "~0.44.0",
|
||||||
|
"@hackmd/markdown-it-task-lists": "~2.1.4",
|
||||||
|
"@hono/standard-validator": "~0.2.0",
|
||||||
|
"@inquirer/confirm": "~6.0.1",
|
||||||
|
"@logtape/file": "~1.2.0",
|
||||||
|
"@logtape/logtape": "~1.2.0",
|
||||||
|
"@logtape/sentry": "~1.2.0",
|
||||||
|
"@logtape/otel": "~1.2.0",
|
||||||
|
"@scalar/hono-api-reference": "~0.9.25",
|
||||||
|
"@sentry/bun": "~10.26.0",
|
||||||
|
"openid-client": "~6.8.1",
|
||||||
|
"altcha-lib": "~1.3.0",
|
||||||
|
"blurhash": "~2.0.5",
|
||||||
|
"bullmq": "~5.64.1",
|
||||||
|
"chalk": "~5.6.2",
|
||||||
|
"clerc": "~0.44.0",
|
||||||
|
"confbox": "~0.2.2",
|
||||||
|
"drizzle-orm": "~0.44.7",
|
||||||
|
"feed": "~5.1.0",
|
||||||
|
"hono": "~4.10.6",
|
||||||
|
"hono-openapi": "~1.1.1",
|
||||||
|
"hono-rate-limiter": "~0.4.2",
|
||||||
|
"html-to-text": "~9.0.5",
|
||||||
|
"ioredis": "~5.8.2",
|
||||||
|
"ip-matching": "~2.1.2",
|
||||||
|
"iso-639-1": "~3.1.5",
|
||||||
|
"linkify-html": "~4.3.2",
|
||||||
|
"linkify-string": "~4.3.2",
|
||||||
|
"linkifyjs": "~4.3.2",
|
||||||
|
"magic-regexp": "~0.10.0",
|
||||||
|
"markdown-it": "~14.1.0",
|
||||||
|
"markdown-it-anchor": "~9.2.0",
|
||||||
|
"markdown-it-container": "~4.0.0",
|
||||||
|
"markdown-it-mathjax3": "~5.2.0",
|
||||||
|
"markdown-it-toc-done-right": "~4.2.0",
|
||||||
|
"mime-types": "~3.0.2",
|
||||||
|
"mitata": "~1.0.34",
|
||||||
|
"ora": "~9.0.0",
|
||||||
|
"qs": "~6.14.0",
|
||||||
|
"sharp": "~0.34.5",
|
||||||
|
"sonic-channel": "~1.3.1",
|
||||||
|
"string-comparison": "~1.3.0",
|
||||||
|
"stringify-entities": "~4.0.4",
|
||||||
|
"unicode-emoji-json": "~0.8.0",
|
||||||
|
"uqr": "~0.1.2",
|
||||||
|
"web-push": "~3.6.7",
|
||||||
|
"xss": "~1.0.15",
|
||||||
|
"youch": "~4.1.0-beta.13",
|
||||||
|
"zod": "~4.1.12",
|
||||||
|
"zod-openapi": "~5.4.3",
|
||||||
|
"zod-validation-error": "~5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"maintainers": [
|
||||||
|
{
|
||||||
|
"email": "contact@cpluspatch.com",
|
||||||
|
"name": "Jesse Wierzbinski",
|
||||||
|
"url": "https://cpluspatch.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/versia-pub/server.git"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"@biomejs/biome",
|
"@biomejs/biome",
|
||||||
"es5-ext",
|
"es5-ext",
|
||||||
|
|
@ -191,7 +190,6 @@
|
||||||
"ioredis": "catalog:",
|
"ioredis": "catalog:",
|
||||||
"ip-matching": "catalog:",
|
"ip-matching": "catalog:",
|
||||||
"iso-639-1": "catalog:",
|
"iso-639-1": "catalog:",
|
||||||
"jose": "catalog:",
|
|
||||||
"linkify-html": "catalog:",
|
"linkify-html": "catalog:",
|
||||||
"linkify-string": "catalog:",
|
"linkify-string": "catalog:",
|
||||||
"linkifyjs": "catalog:",
|
"linkifyjs": "catalog:",
|
||||||
|
|
@ -203,7 +201,6 @@
|
||||||
"markdown-it-toc-done-right": "catalog:",
|
"markdown-it-toc-done-right": "catalog:",
|
||||||
"mime-types": "catalog:",
|
"mime-types": "catalog:",
|
||||||
"mitata": "catalog:",
|
"mitata": "catalog:",
|
||||||
"oauth4webapi": "catalog:",
|
|
||||||
"ora": "catalog:",
|
"ora": "catalog:",
|
||||||
"qs": "catalog:",
|
"qs": "catalog:",
|
||||||
"sharp": "catalog:",
|
"sharp": "catalog:",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import type { ApiError } from "@versia-server/kit";
|
||||||
import { env } from "bun";
|
import { env } from "bun";
|
||||||
import type { MiddlewareHandler } from "hono";
|
import type { MiddlewareHandler } from "hono";
|
||||||
import { rateLimiter } from "hono-rate-limiter";
|
import { rateLimiter } from "hono-rate-limiter";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod";
|
||||||
import type { HonoEnv } from "~/types/api";
|
import type { HonoEnv } from "~/types/api";
|
||||||
|
|
||||||
// Not exported by hono-rate-limiter
|
// Not exported by hono-rate-limiter
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@
|
||||||
"@versia-server/logging": "workspace:*",
|
"@versia-server/logging": "workspace:*",
|
||||||
"@versia/client": "workspace:*",
|
"@versia/client": "workspace:*",
|
||||||
"@versia/sdk": "workspace:*",
|
"@versia/sdk": "workspace:*",
|
||||||
|
"openid-client": "catalog:",
|
||||||
"youch": "catalog:",
|
"youch": "catalog:",
|
||||||
"hono": "catalog:",
|
"hono": "catalog:",
|
||||||
"hono-openapi": "catalog:",
|
"hono-openapi": "catalog:",
|
||||||
|
|
@ -66,7 +67,6 @@
|
||||||
"unicode-emoji-json": "catalog:",
|
"unicode-emoji-json": "catalog:",
|
||||||
"sharp": "catalog:",
|
"sharp": "catalog:",
|
||||||
"iso-639-1": "catalog:",
|
"iso-639-1": "catalog:",
|
||||||
"jose": "catalog:",
|
|
||||||
"zod-openapi": "catalog:",
|
"zod-openapi": "catalog:",
|
||||||
"@scalar/hono-api-reference": "catalog:",
|
"@scalar/hono-api-reference": "catalog:",
|
||||||
"hono-rate-limiter": "catalog:",
|
"hono-rate-limiter": "catalog:",
|
||||||
|
|
@ -75,7 +75,6 @@
|
||||||
"altcha-lib": "catalog:",
|
"altcha-lib": "catalog:",
|
||||||
"@hono/standard-validator": "catalog:",
|
"@hono/standard-validator": "catalog:",
|
||||||
"zod-validation-error": "catalog:",
|
"zod-validation-error": "catalog:",
|
||||||
"confbox": "catalog:",
|
"confbox": "catalog:"
|
||||||
"oauth4webapi": "catalog:"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import type { Context, TypedResponse } from "hono";
|
|
||||||
|
|
||||||
export const errors = {
|
|
||||||
InvalidJWT: ["invalid_request", "Invalid JWT: could not verify"],
|
|
||||||
MissingJWTFields: [
|
|
||||||
"invalid_request",
|
|
||||||
"Invalid JWT: missing required fields (aud, sub, exp, iss)",
|
|
||||||
],
|
|
||||||
InvalidSub: ["invalid_request", "Invalid JWT: sub is not a valid user ID"],
|
|
||||||
UserNotFound: [
|
|
||||||
"invalid_request",
|
|
||||||
"Invalid JWT, could not find associated user",
|
|
||||||
],
|
|
||||||
MissingOauthPermission: [
|
|
||||||
"unauthorized",
|
|
||||||
"User missing required 'oauth' permission",
|
|
||||||
],
|
|
||||||
MissingApplication: [
|
|
||||||
"invalid_request",
|
|
||||||
"Invalid client_id: no associated API application found",
|
|
||||||
],
|
|
||||||
InvalidRedirectUri: [
|
|
||||||
"invalid_request",
|
|
||||||
"Invalid redirect_uri: does not match API application's redirect_uri",
|
|
||||||
],
|
|
||||||
InvalidScope: [
|
|
||||||
"invalid_request",
|
|
||||||
"Invalid scope: not a subset of the application's scopes",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const errorRedirect = (
|
|
||||||
context: Context,
|
|
||||||
error: (typeof errors)[keyof typeof errors],
|
|
||||||
extraParams?: URLSearchParams,
|
|
||||||
): Response & TypedResponse<undefined, 302, "redirect"> => {
|
|
||||||
const errorSearchParams = new URLSearchParams(extraParams);
|
|
||||||
|
|
||||||
errorSearchParams.append("error", error[0]);
|
|
||||||
errorSearchParams.append("error_description", error[1]);
|
|
||||||
|
|
||||||
return context.redirect(
|
|
||||||
`${context.get("config").frontend.routes.login}?${errorSearchParams.toString()}`,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,217 +0,0 @@
|
||||||
import { type Application, db } from "@versia-server/kit/db";
|
|
||||||
import type { OpenIdLoginFlows } from "@versia-server/kit/tables";
|
|
||||||
import { eq, type InferSelectModel, type SQL } from "drizzle-orm";
|
|
||||||
import {
|
|
||||||
type AuthorizationResponseError,
|
|
||||||
type AuthorizationServer,
|
|
||||||
authorizationCodeGrantRequest,
|
|
||||||
ClientSecretPost,
|
|
||||||
discoveryRequest,
|
|
||||||
expectNoState,
|
|
||||||
getValidatedIdTokenClaims,
|
|
||||||
processAuthorizationCodeResponse,
|
|
||||||
processDiscoveryResponse,
|
|
||||||
processUserInfoResponse,
|
|
||||||
type ResponseBodyError,
|
|
||||||
type TokenEndpointResponse,
|
|
||||||
type UserInfoResponse,
|
|
||||||
userInfoRequest,
|
|
||||||
validateAuthResponse,
|
|
||||||
} from "oauth4webapi";
|
|
||||||
|
|
||||||
export const oauthDiscoveryRequest = (
|
|
||||||
issuerUrl: URL,
|
|
||||||
): Promise<AuthorizationServer> => {
|
|
||||||
return discoveryRequest(issuerUrl, {
|
|
||||||
algorithm: "oidc",
|
|
||||||
}).then((res) => processDiscoveryResponse(issuerUrl, res));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const oauthRedirectUri = (baseUrl: URL, issuer: string): URL =>
|
|
||||||
new URL(`/oauth/sso/${issuer}/callback`, baseUrl);
|
|
||||||
|
|
||||||
const getFlow = (
|
|
||||||
flowId: string,
|
|
||||||
): Promise<
|
|
||||||
| (InferSelectModel<typeof OpenIdLoginFlows> & {
|
|
||||||
application?: typeof Application.$type | null;
|
|
||||||
})
|
|
||||||
| undefined
|
|
||||||
> => {
|
|
||||||
return db.query.OpenIdLoginFlows.findFirst({
|
|
||||||
where: (flow): SQL | undefined => eq(flow.id, flowId),
|
|
||||||
with: {
|
|
||||||
application: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAuthServer = (issuerUrl: URL): Promise<AuthorizationServer> => {
|
|
||||||
return discoveryRequest(issuerUrl, {
|
|
||||||
algorithm: "oidc",
|
|
||||||
}).then((res) => processDiscoveryResponse(issuerUrl, res));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getParameters = (
|
|
||||||
authServer: AuthorizationServer,
|
|
||||||
clientId: string,
|
|
||||||
currentUrl: URL,
|
|
||||||
): URLSearchParams => {
|
|
||||||
return validateAuthResponse(
|
|
||||||
authServer,
|
|
||||||
{
|
|
||||||
client_id: clientId,
|
|
||||||
},
|
|
||||||
currentUrl,
|
|
||||||
expectNoState,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOIDCResponse = (
|
|
||||||
authServer: AuthorizationServer,
|
|
||||||
clientId: string,
|
|
||||||
clientSecret: string,
|
|
||||||
redirectUri: URL,
|
|
||||||
codeVerifier: string,
|
|
||||||
parameters: URLSearchParams,
|
|
||||||
): Promise<Response> => {
|
|
||||||
return authorizationCodeGrantRequest(
|
|
||||||
authServer,
|
|
||||||
{
|
|
||||||
client_id: clientId,
|
|
||||||
},
|
|
||||||
ClientSecretPost(clientSecret),
|
|
||||||
parameters,
|
|
||||||
redirectUri.toString(),
|
|
||||||
codeVerifier,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const processOIDCResponse = (
|
|
||||||
authServer: AuthorizationServer,
|
|
||||||
clientId: string,
|
|
||||||
oidcResponse: Response,
|
|
||||||
): Promise<TokenEndpointResponse> => {
|
|
||||||
return processAuthorizationCodeResponse(
|
|
||||||
authServer,
|
|
||||||
{
|
|
||||||
client_id: clientId,
|
|
||||||
},
|
|
||||||
oidcResponse,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserInfo = (
|
|
||||||
authServer: AuthorizationServer,
|
|
||||||
clientId: string,
|
|
||||||
accessToken: string,
|
|
||||||
sub: string,
|
|
||||||
): Promise<UserInfoResponse> => {
|
|
||||||
return userInfoRequest(
|
|
||||||
authServer,
|
|
||||||
{
|
|
||||||
client_id: clientId,
|
|
||||||
},
|
|
||||||
accessToken,
|
|
||||||
).then(
|
|
||||||
async (res) =>
|
|
||||||
await processUserInfoResponse(
|
|
||||||
authServer,
|
|
||||||
{
|
|
||||||
client_id: clientId,
|
|
||||||
},
|
|
||||||
sub,
|
|
||||||
res,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const automaticOidcFlow = async (
|
|
||||||
issuer: {
|
|
||||||
url: string;
|
|
||||||
client_id: string;
|
|
||||||
client_secret: string;
|
|
||||||
},
|
|
||||||
flowId: string,
|
|
||||||
currentUrl: URL,
|
|
||||||
redirectUrl: URL,
|
|
||||||
errorFn: (
|
|
||||||
error: string,
|
|
||||||
message: string,
|
|
||||||
flow:
|
|
||||||
| (InferSelectModel<typeof OpenIdLoginFlows> & {
|
|
||||||
application?: typeof Application.$type | null;
|
|
||||||
})
|
|
||||||
| null,
|
|
||||||
) => Response,
|
|
||||||
): Promise<
|
|
||||||
| Response
|
|
||||||
| {
|
|
||||||
userInfo: UserInfoResponse;
|
|
||||||
flow: InferSelectModel<typeof OpenIdLoginFlows> & {
|
|
||||||
application?: typeof Application.$type | null;
|
|
||||||
};
|
|
||||||
claims: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
> => {
|
|
||||||
const flow = await getFlow(flowId);
|
|
||||||
|
|
||||||
if (!flow) {
|
|
||||||
return errorFn("invalid_request", "Invalid flow", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const issuerUrl = new URL(issuer.url);
|
|
||||||
|
|
||||||
const authServer = await getAuthServer(issuerUrl);
|
|
||||||
|
|
||||||
const parameters = getParameters(
|
|
||||||
authServer,
|
|
||||||
issuer.client_id,
|
|
||||||
currentUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
const oidcResponse = await getOIDCResponse(
|
|
||||||
authServer,
|
|
||||||
issuer.client_id,
|
|
||||||
issuer.client_secret,
|
|
||||||
redirectUrl,
|
|
||||||
flow.codeVerifier,
|
|
||||||
parameters,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await processOIDCResponse(
|
|
||||||
authServer,
|
|
||||||
issuer.client_id,
|
|
||||||
oidcResponse,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { access_token } = result;
|
|
||||||
|
|
||||||
const claims = getValidatedIdTokenClaims(result);
|
|
||||||
|
|
||||||
if (!claims) {
|
|
||||||
return errorFn("invalid_request", "Invalid claims", flow);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { sub } = claims;
|
|
||||||
|
|
||||||
// Validate `sub`
|
|
||||||
// Later, we'll use this to automatically set the user's data
|
|
||||||
const userInfo = await getUserInfo(
|
|
||||||
authServer,
|
|
||||||
issuer.client_id,
|
|
||||||
access_token,
|
|
||||||
sub,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
userInfo,
|
|
||||||
flow,
|
|
||||||
claims,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
const error = e as ResponseBodyError | AuthorizationResponseError;
|
|
||||||
return errorFn(error.error, error.error_description || "", flow);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,229 +0,0 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
|
||||||
import { config } from "@versia-server/config";
|
|
||||||
import { Application } from "@versia-server/kit/db";
|
|
||||||
import { fakeRequest, getTestUsers } from "@versia-server/tests";
|
|
||||||
import { randomUUIDv7 } from "bun";
|
|
||||||
import { randomString } from "@/math";
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,197 +0,0 @@
|
||||||
import { config } from "@versia-server/config";
|
|
||||||
import { ApiError } from "@versia-server/kit";
|
|
||||||
import { apiRoute, handleZodError } from "@versia-server/kit/api";
|
|
||||||
import { Application, User } from "@versia-server/kit/db";
|
|
||||||
import { Users } from "@versia-server/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, validator } from "hono-openapi";
|
|
||||||
import { SignJWT } from "jose";
|
|
||||||
import { z } from "zod/v4";
|
|
||||||
|
|
||||||
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.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
|
|
||||||
.email()
|
|
||||||
.toLowerCase()
|
|
||||||
.or(z.string().toLowerCase()),
|
|
||||||
password: z.string().min(2).max(100),
|
|
||||||
}),
|
|
||||||
handleZodError,
|
|
||||||
),
|
|
||||||
async (context) => {
|
|
||||||
if (config.authentication.forced_openid) {
|
|
||||||
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()}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(config.authentication.keys.private);
|
|
||||||
|
|
||||||
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()}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
import { config } from "@versia-server/config";
|
|
||||||
import { apiRoute, handleZodError } from "@versia-server/kit/api";
|
|
||||||
import { db } from "@versia-server/kit/db";
|
|
||||||
import { Applications, Tokens } from "@versia-server/kit/tables";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
|
||||||
import { describeRoute, validator } from "hono-openapi";
|
|
||||||
import { z } from "zod/v4";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.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()}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
|
||||||
import { config } from "@versia-server/config";
|
|
||||||
import { Application } from "@versia-server/kit/db";
|
|
||||||
import { fakeRequest, getTestUsers } from "@versia-server/tests";
|
|
||||||
import { randomUUIDv7 } from "bun";
|
|
||||||
import { randomString } from "@/math";
|
|
||||||
|
|
||||||
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=[^;]+;/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
import { config } from "@versia-server/config";
|
|
||||||
import { apiRoute, handleZodError } from "@versia-server/kit/api";
|
|
||||||
import { User } from "@versia-server/kit/db";
|
|
||||||
import { Users } from "@versia-server/kit/tables";
|
|
||||||
import { password as bunPassword } from "bun";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import type { Context } from "hono";
|
|
||||||
import { describeRoute, validator } from "hono-openapi";
|
|
||||||
import { z } from "zod/v4";
|
|
||||||
|
|
||||||
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,394 +0,0 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
|
||||||
import { RolePermission } from "@versia/client/schemas";
|
|
||||||
import { config } from "@versia-server/config";
|
|
||||||
import { Application } from "@versia-server/kit/db";
|
|
||||||
import { fakeRequest, getTestUsers } from "@versia-server/tests";
|
|
||||||
import { randomUUIDv7 } from "bun";
|
|
||||||
import { SignJWT } from "jose";
|
|
||||||
import { randomString } from "@/math";
|
|
||||||
|
|
||||||
const { deleteUsers, tokens, users } = await getTestUsers(1);
|
|
||||||
|
|
||||||
const application = await Application.insert({
|
|
||||||
id: randomUUIDv7(),
|
|
||||||
clientId: "test-client-id",
|
|
||||||
redirectUri: "https://example.com/callback",
|
|
||||||
scopes: "openid profile email",
|
|
||||||
name: "Test Application",
|
|
||||||
secret: "test-secret",
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await deleteUsers();
|
|
||||||
await application.delete();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("/oauth/authorize", () => {
|
|
||||||
test("should authorize and redirect with valid inputs", async () => {
|
|
||||||
const jwt = await new SignJWT({
|
|
||||||
sub: users[0].id,
|
|
||||||
iss: config.http.base_url.origin,
|
|
||||||
aud: application.data.clientId,
|
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
|
||||||
iat: Math.floor(Date.now() / 1000),
|
|
||||||
nbf: Math.floor(Date.now() / 1000),
|
|
||||||
})
|
|
||||||
.setProtectedHeader({ alg: "EdDSA" })
|
|
||||||
.sign(config.authentication.keys.private);
|
|
||||||
|
|
||||||
const response = await fakeRequest("/oauth/authorize", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${tokens[0].data.accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Cookie: `jwt=${jwt}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
client_id: application.data.clientId,
|
|
||||||
redirect_uri: application.data.redirectUri,
|
|
||||||
response_type: "code",
|
|
||||||
scope: application.data.scopes,
|
|
||||||
state: "test-state",
|
|
||||||
code_challenge: randomString(43),
|
|
||||||
code_challenge_method: "S256",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).toBe(302);
|
|
||||||
const location = new URL(
|
|
||||||
response.headers.get("Location") ?? "",
|
|
||||||
config.http.base_url,
|
|
||||||
);
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
expect(location.origin + location.pathname).toBe(
|
|
||||||
application.data.redirectUri,
|
|
||||||
);
|
|
||||||
expect(params.get("code")).toBeTruthy();
|
|
||||||
expect(params.get("state")).toBe("test-state");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return error for invalid JWT", async () => {
|
|
||||||
const response = await fakeRequest("/oauth/authorize", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${tokens[0].data.accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Cookie: "jwt=invalid-jwt",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
client_id: application.data.clientId,
|
|
||||||
redirect_uri: application.data.redirectUri,
|
|
||||||
response_type: "code",
|
|
||||||
scope: application.data.scopes,
|
|
||||||
state: "test-state",
|
|
||||||
code_challenge: randomString(43),
|
|
||||||
code_challenge_method: "S256",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).toBe(302);
|
|
||||||
const location = new URL(
|
|
||||||
response.headers.get("Location") ?? "",
|
|
||||||
config.http.base_url,
|
|
||||||
);
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
expect(params.get("error")).toBe("invalid_request");
|
|
||||||
expect(params.get("error_description")).toBe(
|
|
||||||
"Invalid JWT: could not verify",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return error for missing required fields in JWT", async () => {
|
|
||||||
const jwt = await new SignJWT({
|
|
||||||
sub: users[0].id,
|
|
||||||
iss: config.http.base_url.origin,
|
|
||||||
aud: application.data.clientId,
|
|
||||||
})
|
|
||||||
.setProtectedHeader({ alg: "EdDSA" })
|
|
||||||
.sign(config.authentication.keys.private);
|
|
||||||
|
|
||||||
const response = await fakeRequest("/oauth/authorize", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${tokens[0].data.accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Cookie: `jwt=${jwt}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
client_id: application.data.clientId,
|
|
||||||
redirect_uri: application.data.redirectUri,
|
|
||||||
response_type: "code",
|
|
||||||
scope: application.data.scopes,
|
|
||||||
state: "test-state",
|
|
||||||
code_challenge: randomString(43),
|
|
||||||
code_challenge_method: "S256",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).toBe(302);
|
|
||||||
const location = new URL(
|
|
||||||
response.headers.get("Location") ?? "",
|
|
||||||
config.http.base_url,
|
|
||||||
);
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
expect(params.get("error")).toBe("invalid_request");
|
|
||||||
expect(params.get("error_description")).toBe(
|
|
||||||
"Invalid JWT: missing required fields (aud, sub, exp, iss)",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return error for user not found", async () => {
|
|
||||||
const jwt = await new SignJWT({
|
|
||||||
sub: "non-existent-user",
|
|
||||||
aud: application.data.clientId,
|
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
|
||||||
iss: config.http.base_url.origin,
|
|
||||||
iat: Math.floor(Date.now() / 1000),
|
|
||||||
nbf: Math.floor(Date.now() / 1000),
|
|
||||||
})
|
|
||||||
.setProtectedHeader({ alg: "EdDSA" })
|
|
||||||
.sign(config.authentication.keys.private);
|
|
||||||
|
|
||||||
const response = await fakeRequest("/oauth/authorize", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${tokens[0].data.accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Cookie: `jwt=${jwt}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
client_id: application.data.clientId,
|
|
||||||
redirect_uri: application.data.redirectUri,
|
|
||||||
response_type: "code",
|
|
||||||
scope: application.data.scopes,
|
|
||||||
state: "test-state",
|
|
||||||
code_challenge: randomString(43),
|
|
||||||
code_challenge_method: "S256",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).toBe(302);
|
|
||||||
const location = new URL(
|
|
||||||
response.headers.get("Location") ?? "",
|
|
||||||
config.http.base_url,
|
|
||||||
);
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
expect(params.get("error")).toBe("invalid_request");
|
|
||||||
expect(params.get("error_description")).toBe(
|
|
||||||
"Invalid JWT: sub is not a valid user ID",
|
|
||||||
);
|
|
||||||
|
|
||||||
const jwt2 = await new SignJWT({
|
|
||||||
sub: "23e42862-d5df-49a8-95b5-52d8c6a11aea",
|
|
||||||
aud: application.data.clientId,
|
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
|
||||||
iss: config.http.base_url.origin,
|
|
||||||
iat: Math.floor(Date.now() / 1000),
|
|
||||||
nbf: Math.floor(Date.now() / 1000),
|
|
||||||
})
|
|
||||||
.setProtectedHeader({ alg: "EdDSA" })
|
|
||||||
.sign(config.authentication.keys.private);
|
|
||||||
|
|
||||||
const response2 = await fakeRequest("/oauth/authorize", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${tokens[0].data.accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Cookie: `jwt=${jwt2}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
client_id: application.data.clientId,
|
|
||||||
redirect_uri: application.data.redirectUri,
|
|
||||||
response_type: "code",
|
|
||||||
scope: application.data.scopes,
|
|
||||||
state: "test-state",
|
|
||||||
code_challenge: randomString(43),
|
|
||||||
code_challenge_method: "S256",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response2.status).toBe(302);
|
|
||||||
const location2 = new URL(
|
|
||||||
response2.headers.get("Location") ?? "",
|
|
||||||
config.http.base_url,
|
|
||||||
);
|
|
||||||
const params2 = new URLSearchParams(location2.search);
|
|
||||||
expect(params2.get("error")).toBe("invalid_request");
|
|
||||||
expect(params2.get("error_description")).toBe(
|
|
||||||
"Invalid JWT, could not find associated user",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return error for user missing required permissions", async () => {
|
|
||||||
const oldPermissions = config.permissions.default;
|
|
||||||
config.permissions.default = [];
|
|
||||||
|
|
||||||
const jwt = await new SignJWT({
|
|
||||||
sub: users[0].id,
|
|
||||||
iss: config.http.base_url.origin,
|
|
||||||
aud: application.data.clientId,
|
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
|
||||||
iat: Math.floor(Date.now() / 1000),
|
|
||||||
nbf: Math.floor(Date.now() / 1000),
|
|
||||||
})
|
|
||||||
.setProtectedHeader({ alg: "EdDSA" })
|
|
||||||
.sign(config.authentication.keys.private);
|
|
||||||
|
|
||||||
const response = await fakeRequest("/oauth/authorize", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${tokens[0].data.accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Cookie: `jwt=${jwt}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
client_id: application.data.clientId,
|
|
||||||
redirect_uri: application.data.redirectUri,
|
|
||||||
response_type: "code",
|
|
||||||
scope: application.data.scopes,
|
|
||||||
state: "test-state",
|
|
||||||
code_challenge: randomString(43),
|
|
||||||
code_challenge_method: "S256",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).toBe(302);
|
|
||||||
const location = new URL(
|
|
||||||
response.headers.get("Location") ?? "",
|
|
||||||
config.http.base_url,
|
|
||||||
);
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
expect(params.get("error")).toBe("unauthorized");
|
|
||||||
expect(params.get("error_description")).toBe(
|
|
||||||
`User missing required '${RolePermission.OAuth}' permission`,
|
|
||||||
);
|
|
||||||
|
|
||||||
config.permissions.default = oldPermissions;
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return error for invalid client_id", async () => {
|
|
||||||
const jwt = await new SignJWT({
|
|
||||||
sub: users[0].id,
|
|
||||||
aud: "invalid-client-id",
|
|
||||||
iss: config.http.base_url.origin,
|
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
|
||||||
iat: Math.floor(Date.now() / 1000),
|
|
||||||
nbf: Math.floor(Date.now() / 1000),
|
|
||||||
})
|
|
||||||
.setProtectedHeader({ alg: "EdDSA" })
|
|
||||||
.sign(config.authentication.keys.private);
|
|
||||||
|
|
||||||
const response = await fakeRequest("/oauth/authorize", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${tokens[0].data.accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Cookie: `jwt=${jwt}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
client_id: "invalid-client-id",
|
|
||||||
redirect_uri: application.data.redirectUri,
|
|
||||||
response_type: "code",
|
|
||||||
scope: application.data.scopes,
|
|
||||||
state: "test-state",
|
|
||||||
code_challenge: randomString(43),
|
|
||||||
code_challenge_method: "S256",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).toBe(302);
|
|
||||||
const location = new URL(
|
|
||||||
response.headers.get("Location") ?? "",
|
|
||||||
config.http.base_url,
|
|
||||||
);
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
expect(params.get("error")).toBe("invalid_request");
|
|
||||||
expect(params.get("error_description")).toBe(
|
|
||||||
"Invalid client_id: no associated API application found",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return error for invalid redirect_uri", async () => {
|
|
||||||
const jwt = await new SignJWT({
|
|
||||||
sub: users[0].id,
|
|
||||||
iss: config.http.base_url.origin,
|
|
||||||
aud: application.data.clientId,
|
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
|
||||||
iat: Math.floor(Date.now() / 1000),
|
|
||||||
nbf: Math.floor(Date.now() / 1000),
|
|
||||||
})
|
|
||||||
.setProtectedHeader({ alg: "EdDSA" })
|
|
||||||
.sign(config.authentication.keys.private);
|
|
||||||
|
|
||||||
const response = await fakeRequest("/oauth/authorize", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${tokens[0].data.accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Cookie: `jwt=${jwt}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
client_id: application.data.clientId,
|
|
||||||
redirect_uri: "https://invalid.com/callback",
|
|
||||||
response_type: "code",
|
|
||||||
scope: application.data.scopes,
|
|
||||||
state: "test-state",
|
|
||||||
code_challenge: randomString(43),
|
|
||||||
code_challenge_method: "S256",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).toBe(302);
|
|
||||||
const location = new URL(
|
|
||||||
response.headers.get("Location") ?? "",
|
|
||||||
config.http.base_url,
|
|
||||||
);
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
expect(params.get("error")).toBe("invalid_request");
|
|
||||||
expect(params.get("error_description")).toBe(
|
|
||||||
"Invalid redirect_uri: does not match API application's redirect_uri",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return error for invalid scope", async () => {
|
|
||||||
const jwt = await new SignJWT({
|
|
||||||
sub: users[0].id,
|
|
||||||
iss: config.http.base_url.origin,
|
|
||||||
aud: application.data.clientId,
|
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
|
||||||
iat: Math.floor(Date.now() / 1000),
|
|
||||||
nbf: Math.floor(Date.now() / 1000),
|
|
||||||
})
|
|
||||||
.setProtectedHeader({ alg: "EdDSA" })
|
|
||||||
.sign(config.authentication.keys.private);
|
|
||||||
|
|
||||||
const response = await fakeRequest("/oauth/authorize", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${tokens[0].data.accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Cookie: `jwt=${jwt}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
client_id: application.data.clientId,
|
|
||||||
redirect_uri: application.data.redirectUri,
|
|
||||||
response_type: "code",
|
|
||||||
scope: "invalid-scope",
|
|
||||||
state: "test-state",
|
|
||||||
code_challenge: randomString(43),
|
|
||||||
code_challenge_method: "S256",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).toBe(302);
|
|
||||||
const location = new URL(
|
|
||||||
response.headers.get("Location") ?? "",
|
|
||||||
config.http.base_url,
|
|
||||||
);
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
expect(params.get("error")).toBe("invalid_request");
|
|
||||||
expect(params.get("error_description")).toBe(
|
|
||||||
"Invalid scope: not a subset of the application's scopes",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,277 +0,0 @@
|
||||||
import { RolePermission } from "@versia/client/schemas";
|
|
||||||
import { config } from "@versia-server/config";
|
|
||||||
import {
|
|
||||||
apiRoute,
|
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
jsonOrForm,
|
|
||||||
} from "@versia-server/kit/api";
|
|
||||||
import { Application, Token, User } from "@versia-server/kit/db";
|
|
||||||
import { randomUUIDv7 } from "bun";
|
|
||||||
import { describeRoute, validator } from "hono-openapi";
|
|
||||||
import { type JWTPayload, jwtVerify, SignJWT } from "jose";
|
|
||||||
import { JOSEError } from "jose/errors";
|
|
||||||
import { z } from "zod/v4";
|
|
||||||
import { randomString } from "@/math";
|
|
||||||
import { errorRedirect, errors } from "../../../plugins/openid/errors.ts";
|
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
|
||||||
app.post(
|
|
||||||
"/oauth/authorize",
|
|
||||||
describeRoute({
|
|
||||||
summary: "Main OpenID authorization endpoint",
|
|
||||||
tags: ["OpenID"],
|
|
||||||
responses: {
|
|
||||||
302: {
|
|
||||||
description: "Redirect to the application",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
auth({
|
|
||||||
auth: false,
|
|
||||||
}),
|
|
||||||
jsonOrForm(),
|
|
||||||
validator(
|
|
||||||
"query",
|
|
||||||
z.object({
|
|
||||||
prompt: z
|
|
||||||
.enum(["none", "login", "consent", "select_account"])
|
|
||||||
.optional()
|
|
||||||
.default("none"),
|
|
||||||
max_age: z.coerce
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.optional()
|
|
||||||
.default(60 * 60 * 24 * 7),
|
|
||||||
}),
|
|
||||||
handleZodError,
|
|
||||||
),
|
|
||||||
validator(
|
|
||||||
"json",
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
scope: z.string().optional(),
|
|
||||||
redirect_uri: z
|
|
||||||
.url()
|
|
||||||
.optional()
|
|
||||||
.or(z.literal("urn:ietf:wg:oauth:2.0:oob")),
|
|
||||||
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(),
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
// Check if redirect_uri is valid for code flow
|
|
||||||
(data) =>
|
|
||||||
data.response_type.includes("code")
|
|
||||||
? data.redirect_uri
|
|
||||||
: true,
|
|
||||||
"redirect_uri is required for code flow",
|
|
||||||
),
|
|
||||||
// Disable for Mastodon API compatibility
|
|
||||||
/* .refine(
|
|
||||||
// Check if code_challenge is valid for code flow
|
|
||||||
(data) =>
|
|
||||||
data.response_type.includes("code")
|
|
||||||
? data.code_challenge
|
|
||||||
: true,
|
|
||||||
"code_challenge is required for code flow",
|
|
||||||
), */
|
|
||||||
handleZodError,
|
|
||||||
),
|
|
||||||
validator(
|
|
||||||
"cookie",
|
|
||||||
z.object({
|
|
||||||
jwt: z.string(),
|
|
||||||
}),
|
|
||||||
handleZodError,
|
|
||||||
),
|
|
||||||
async (context) => {
|
|
||||||
const { scope, redirect_uri, client_id, state } =
|
|
||||||
context.req.valid("json");
|
|
||||||
|
|
||||||
const { jwt } = context.req.valid("cookie");
|
|
||||||
|
|
||||||
const errorSearchParams = new URLSearchParams(
|
|
||||||
context.req.valid("json"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await jwtVerify(
|
|
||||||
jwt,
|
|
||||||
config.authentication.keys.public,
|
|
||||||
{
|
|
||||||
algorithms: ["EdDSA"],
|
|
||||||
audience: client_id,
|
|
||||||
issuer: new URL(context.get("config").http.base_url).origin,
|
|
||||||
},
|
|
||||||
).catch((error) => {
|
|
||||||
if (error instanceof JOSEError) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
return errorRedirect(
|
|
||||||
context,
|
|
||||||
errors.InvalidJWT,
|
|
||||||
errorSearchParams,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
payload: { aud, sub, exp },
|
|
||||||
} = result;
|
|
||||||
|
|
||||||
if (!(aud && sub && exp)) {
|
|
||||||
return errorRedirect(
|
|
||||||
context,
|
|
||||||
errors.MissingJWTFields,
|
|
||||||
errorSearchParams,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!z.uuid().safeParse(sub).success) {
|
|
||||||
return errorRedirect(
|
|
||||||
context,
|
|
||||||
errors.InvalidSub,
|
|
||||||
errorSearchParams,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await User.fromId(sub);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return errorRedirect(
|
|
||||||
context,
|
|
||||||
errors.UserNotFound,
|
|
||||||
errorSearchParams,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.hasPermission(RolePermission.OAuth)) {
|
|
||||||
return errorRedirect(
|
|
||||||
context,
|
|
||||||
errors.MissingOauthPermission,
|
|
||||||
errorSearchParams,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const application = await Application.fromClientId(client_id);
|
|
||||||
|
|
||||||
if (!application) {
|
|
||||||
return errorRedirect(
|
|
||||||
context,
|
|
||||||
errors.MissingApplication,
|
|
||||||
errorSearchParams,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (application.data.redirectUri !== redirect_uri) {
|
|
||||||
return errorRedirect(
|
|
||||||
context,
|
|
||||||
errors.InvalidRedirectUri,
|
|
||||||
errorSearchParams,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that scopes are a subset of the application's scopes
|
|
||||||
if (
|
|
||||||
scope &&
|
|
||||||
!scope
|
|
||||||
.split(" ")
|
|
||||||
.every((s) => application.data.scopes.includes(s))
|
|
||||||
) {
|
|
||||||
return errorRedirect(
|
|
||||||
context,
|
|
||||||
errors.InvalidScope,
|
|
||||||
errorSearchParams,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = randomString(256, "base64url");
|
|
||||||
|
|
||||||
let payload: JWTPayload = {};
|
|
||||||
|
|
||||||
if (scope) {
|
|
||||||
if (scope.split(" ").includes("openid")) {
|
|
||||||
payload = {
|
|
||||||
...payload,
|
|
||||||
sub: user.id,
|
|
||||||
iss: new URL(context.get("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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (scope.split(" ").includes("profile")) {
|
|
||||||
payload = {
|
|
||||||
...payload,
|
|
||||||
name: user.data.displayName,
|
|
||||||
preferred_username: user.data.username,
|
|
||||||
picture: user.getAvatarUrl().href,
|
|
||||||
updated_at: new Date(user.data.updatedAt).toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (scope.split(" ").includes("email")) {
|
|
||||||
payload = {
|
|
||||||
...payload,
|
|
||||||
email: user.data.email,
|
|
||||||
// TODO: Add verification system
|
|
||||||
email_verified: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const idToken = await new SignJWT(payload)
|
|
||||||
.setProtectedHeader({ alg: "EdDSA" })
|
|
||||||
.sign(config.authentication.keys.private);
|
|
||||||
|
|
||||||
await Token.insert({
|
|
||||||
id: randomUUIDv7(),
|
|
||||||
accessToken: randomString(64, "base64url"),
|
|
||||||
code,
|
|
||||||
scope: scope ?? application.data.scopes,
|
|
||||||
tokenType: "Bearer",
|
|
||||||
applicationId: application.id,
|
|
||||||
redirectUri: redirect_uri ?? application.data.redirectUri,
|
|
||||||
expiresAt: new Date(
|
|
||||||
Date.now() + 60 * 60 * 24 * 14,
|
|
||||||
).toISOString(),
|
|
||||||
idToken: ["profile", "email", "openid"].some((s) =>
|
|
||||||
scope?.split(" ").includes(s),
|
|
||||||
)
|
|
||||||
? idToken
|
|
||||||
: null,
|
|
||||||
clientId: client_id,
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const redirectUri =
|
|
||||||
redirect_uri === "urn:ietf:wg:oauth:2.0:oob"
|
|
||||||
? new URL(
|
|
||||||
"/oauth/code",
|
|
||||||
context.get("config").http.base_url,
|
|
||||||
)
|
|
||||||
: new URL(redirect_uri ?? application.data.redirectUri);
|
|
||||||
|
|
||||||
redirectUri.searchParams.append("code", code);
|
|
||||||
state && redirectUri.searchParams.append("state", state);
|
|
||||||
|
|
||||||
return context.redirect(redirectUri.toString());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
import { config } from "@versia-server/config";
|
|
||||||
import { apiRoute, handleZodError } from "@versia-server/kit/api";
|
|
||||||
import { Application, db } from "@versia-server/kit/db";
|
|
||||||
import { OpenIdLoginFlows } from "@versia-server/kit/tables";
|
|
||||||
import { randomUUIDv7 } from "bun";
|
|
||||||
import { describeRoute, validator } from "hono-openapi";
|
|
||||||
import {
|
|
||||||
calculatePKCECodeChallenge,
|
|
||||||
discoveryRequest,
|
|
||||||
generateRandomCodeVerifier,
|
|
||||||
processDiscoveryResponse,
|
|
||||||
} from "oauth4webapi";
|
|
||||||
import { z } from "zod/v4";
|
|
||||||
import { oauthRedirectUri } from "../../../plugins/openid/utils.ts";
|
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
|
||||||
app.get(
|
|
||||||
"/oauth/sso",
|
|
||||||
describeRoute({
|
|
||||||
summary: "Initiate SSO login flow",
|
|
||||||
tags: ["OpenID"],
|
|
||||||
responses: {
|
|
||||||
302: {
|
|
||||||
description:
|
|
||||||
"Redirect to SSO login, or redirect to login page with error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
validator(
|
|
||||||
"query",
|
|
||||||
z.object({
|
|
||||||
issuer: z.string(),
|
|
||||||
client_id: z.string().optional(),
|
|
||||||
redirect_uri: z.url().optional(),
|
|
||||||
scope: z.string().optional(),
|
|
||||||
response_type: z.enum(["code"]).optional(),
|
|
||||||
}),
|
|
||||||
handleZodError,
|
|
||||||
),
|
|
||||||
async (context) => {
|
|
||||||
// This is the Versia client's client_id, not the external OAuth provider's client_id
|
|
||||||
const { issuer: issuerId, client_id } = context.req.valid("query");
|
|
||||||
|
|
||||||
const errorSearchParams = new URLSearchParams(
|
|
||||||
context.req.valid("query"),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!client_id || client_id === "undefined") {
|
|
||||||
errorSearchParams.append("error", "invalid_request");
|
|
||||||
errorSearchParams.append(
|
|
||||||
"error_description",
|
|
||||||
"client_id is required",
|
|
||||||
);
|
|
||||||
|
|
||||||
return context.redirect(
|
|
||||||
`${context.get("config").frontend.routes.login}?${errorSearchParams.toString()}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const issuer = config.authentication.openid_providers.find(
|
|
||||||
(provider) => provider.id === issuerId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!issuer) {
|
|
||||||
errorSearchParams.append("error", "invalid_request");
|
|
||||||
errorSearchParams.append(
|
|
||||||
"error_description",
|
|
||||||
"issuer is invalid",
|
|
||||||
);
|
|
||||||
|
|
||||||
return context.redirect(
|
|
||||||
`${context.get("config").frontend.routes.login}?${errorSearchParams.toString()}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const issuerUrl = new URL(issuer.url);
|
|
||||||
|
|
||||||
const authServer = await discoveryRequest(issuerUrl, {
|
|
||||||
algorithm: "oidc",
|
|
||||||
}).then((res) => processDiscoveryResponse(issuerUrl, res));
|
|
||||||
|
|
||||||
const codeVerifier = generateRandomCodeVerifier();
|
|
||||||
|
|
||||||
const application = await Application.fromClientId(client_id);
|
|
||||||
|
|
||||||
if (!application) {
|
|
||||||
errorSearchParams.append("error", "invalid_request");
|
|
||||||
errorSearchParams.append(
|
|
||||||
"error_description",
|
|
||||||
"client_id is invalid",
|
|
||||||
);
|
|
||||||
|
|
||||||
return context.redirect(
|
|
||||||
`${context.get("config").frontend.routes.login}?${errorSearchParams.toString()}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store into database
|
|
||||||
const newFlow = (
|
|
||||||
await db
|
|
||||||
.insert(OpenIdLoginFlows)
|
|
||||||
.values({
|
|
||||||
id: randomUUIDv7(),
|
|
||||||
codeVerifier,
|
|
||||||
applicationId: application.id,
|
|
||||||
issuerId,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
const codeChallenge =
|
|
||||||
await calculatePKCECodeChallenge(codeVerifier);
|
|
||||||
|
|
||||||
return context.redirect(
|
|
||||||
`${authServer.authorization_endpoint}?${new URLSearchParams({
|
|
||||||
client_id: issuer.client_id,
|
|
||||||
redirect_uri: `${oauthRedirectUri(
|
|
||||||
context.get("config").http.base_url,
|
|
||||||
issuerId,
|
|
||||||
)}?flow=${newFlow.id}`,
|
|
||||||
response_type: "code",
|
|
||||||
scope: "openid profile email",
|
|
||||||
// PKCE
|
|
||||||
code_challenge_method: "S256",
|
|
||||||
code_challenge: codeChallenge,
|
|
||||||
}).toString()}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
import { apiRoute, handleZodError, jsonOrForm } from "@versia-server/kit/api";
|
|
||||||
import { Application, Token } from "@versia-server/kit/db";
|
|
||||||
import { Tokens } from "@versia-server/kit/tables";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
||||||
import { z } from "zod/v4";
|
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
|
||||||
app.post(
|
|
||||||
"/oauth/token",
|
|
||||||
describeRoute({
|
|
||||||
summary: "Get token",
|
|
||||||
tags: ["OpenID"],
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Token",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(
|
|
||||||
z.object({
|
|
||||||
access_token: z.string(),
|
|
||||||
token_type: z.string(),
|
|
||||||
expires_in: z
|
|
||||||
.number()
|
|
||||||
.optional()
|
|
||||||
.nullable(),
|
|
||||||
id_token: z.string().optional().nullable(),
|
|
||||||
refresh_token: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.nullable(),
|
|
||||||
scope: z.string().optional(),
|
|
||||||
created_at: z.number(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
401: {
|
|
||||||
description: "Authorization error",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(
|
|
||||||
z.object({
|
|
||||||
error: z.string(),
|
|
||||||
error_description: z.string(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
jsonOrForm(),
|
|
||||||
validator(
|
|
||||||
"json",
|
|
||||||
z.object({
|
|
||||||
code: z.string().optional(),
|
|
||||||
code_verifier: z.string().optional(),
|
|
||||||
grant_type: z
|
|
||||||
.enum([
|
|
||||||
"authorization_code",
|
|
||||||
"refresh_token",
|
|
||||||
"client_credentials",
|
|
||||||
"password",
|
|
||||||
"urn:ietf:params:oauth:grant-type:device_code",
|
|
||||||
"urn:ietf:params:oauth:grant-type:token-exchange",
|
|
||||||
"urn:ietf:params:oauth:grant-type:saml2-bearer",
|
|
||||||
"urn:openid:params:grant-type:ciba",
|
|
||||||
])
|
|
||||||
.default("authorization_code"),
|
|
||||||
client_id: z.string().optional(),
|
|
||||||
client_secret: z.string().optional(),
|
|
||||||
username: z.string().trim().optional(),
|
|
||||||
password: z.string().trim().optional(),
|
|
||||||
redirect_uri: z.url().optional(),
|
|
||||||
refresh_token: z.string().optional(),
|
|
||||||
scope: z.string().optional(),
|
|
||||||
assertion: z.string().optional(),
|
|
||||||
audience: z.string().optional(),
|
|
||||||
subject_token_type: z.string().optional(),
|
|
||||||
subject_token: z.string().optional(),
|
|
||||||
actor_token_type: z.string().optional(),
|
|
||||||
actor_token: z.string().optional(),
|
|
||||||
auth_req_id: z.string().optional(),
|
|
||||||
}),
|
|
||||||
handleZodError,
|
|
||||||
),
|
|
||||||
async (context) => {
|
|
||||||
const { grant_type, code, redirect_uri, client_id, client_secret } =
|
|
||||||
context.req.valid("json");
|
|
||||||
|
|
||||||
switch (grant_type) {
|
|
||||||
case "authorization_code": {
|
|
||||||
if (!code) {
|
|
||||||
return context.json(
|
|
||||||
{
|
|
||||||
error: "invalid_request",
|
|
||||||
error_description: "Code is required",
|
|
||||||
},
|
|
||||||
401,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!redirect_uri) {
|
|
||||||
return context.json(
|
|
||||||
{
|
|
||||||
error: "invalid_request",
|
|
||||||
error_description: "Redirect URI is required",
|
|
||||||
},
|
|
||||||
401,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!client_id) {
|
|
||||||
return context.json(
|
|
||||||
{
|
|
||||||
error: "invalid_request",
|
|
||||||
error_description: "Client ID is required",
|
|
||||||
},
|
|
||||||
401,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the client_secret
|
|
||||||
const client = await Application.fromClientId(client_id);
|
|
||||||
|
|
||||||
if (!client || client.data.secret !== client_secret) {
|
|
||||||
return context.json(
|
|
||||||
{
|
|
||||||
error: "invalid_client",
|
|
||||||
error_description: "Invalid client credentials",
|
|
||||||
},
|
|
||||||
401,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await Token.fromSql(
|
|
||||||
and(
|
|
||||||
eq(Tokens.code, code),
|
|
||||||
eq(Tokens.redirectUri, decodeURI(redirect_uri)),
|
|
||||||
eq(Tokens.clientId, client_id),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return context.json(
|
|
||||||
{
|
|
||||||
error: "invalid_grant",
|
|
||||||
error_description: "Code not found",
|
|
||||||
},
|
|
||||||
401,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate the code
|
|
||||||
await token.update({ code: null });
|
|
||||||
|
|
||||||
return context.json(
|
|
||||||
{
|
|
||||||
...token.toApi(),
|
|
||||||
expires_in: token.data.expiresAt
|
|
||||||
? Math.floor(
|
|
||||||
(new Date(
|
|
||||||
token.data.expiresAt,
|
|
||||||
).getTime() -
|
|
||||||
Date.now()) /
|
|
||||||
1000,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
id_token: token.data.idToken,
|
|
||||||
refresh_token: null,
|
|
||||||
},
|
|
||||||
200,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.json(
|
|
||||||
{
|
|
||||||
error: "unsupported_grant_type",
|
|
||||||
error_description: "Unsupported grant type",
|
|
||||||
},
|
|
||||||
401,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
withUserParam,
|
withUserParam,
|
||||||
} from "@versia-server/kit/api";
|
} from "@versia-server/kit/api";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
import { getFeed } from "@/rss";
|
import { getFeed } from "@/rss";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
withUserParam,
|
withUserParam,
|
||||||
} from "@versia-server/kit/api";
|
} from "@versia-server/kit/api";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
import { getFeed } from "@/rss";
|
import { getFeed } from "@/rss";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from "@versia-server/kit/api";
|
} from "@versia-server/kit/api";
|
||||||
import { Relationship } from "@versia-server/kit/db";
|
import { Relationship } from "@versia-server/kit/db";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { Timeline } from "@versia-server/kit/db";
|
||||||
import { Users } from "@versia-server/kit/tables";
|
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, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { Timeline } from "@versia-server/kit/db";
|
||||||
import { Users } from "@versia-server/kit/tables";
|
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, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import {
|
||||||
relationshipQueue,
|
relationshipQueue,
|
||||||
} from "@versia-server/kit/queues/relationships";
|
} from "@versia-server/kit/queues/relationships";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
} from "@versia-server/kit/api";
|
} from "@versia-server/kit/api";
|
||||||
import { Relationship } from "@versia-server/kit/db";
|
import { Relationship } from "@versia-server/kit/db";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from "@versia-server/kit/api";
|
} from "@versia-server/kit/api";
|
||||||
import { Role } from "@versia-server/kit/db";
|
import { Role } from "@versia-server/kit/db";
|
||||||
import { describeRoute, validator } from "hono-openapi";
|
import { describeRoute, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.post(
|
app.post(
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Role as RoleSchema } from "@versia/client/schemas";
|
||||||
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
||||||
import { Role } from "@versia-server/kit/db";
|
import { Role } from "@versia-server/kit/db";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { describeRoute, resolver } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { Timeline } from "@versia-server/kit/db";
|
||||||
import { Notes } from "@versia-server/kit/tables";
|
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, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { db, User } from "@versia-server/kit/db";
|
||||||
import type { Users } from "@versia-server/kit/tables";
|
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, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ 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, resolver, validator } from "hono-openapi";
|
||||||
import ISO6391 from "iso-639-1";
|
import ISO6391 from "iso-639-1";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
import { tempmailDomains } from "@/tempmail";
|
import { tempmailDomains } from "@/tempmail";
|
||||||
import { rateLimit } from "../../../../middlewares/rate-limit.ts";
|
import { rateLimit } from "../../../../middlewares/rate-limit.ts";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { parseUserAddress } from "@versia-server/kit/parsers";
|
||||||
import { Users } from "@versia-server/kit/tables";
|
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, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
} from "@versia-server/kit/api";
|
} from "@versia-server/kit/api";
|
||||||
import { Relationship } from "@versia-server/kit/db";
|
import { Relationship } from "@versia-server/kit/db";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ 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, resolver, validator } from "hono-openapi";
|
||||||
import stringComparison from "string-comparison";
|
import stringComparison from "string-comparison";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { versiaTextToHtml } from "@versia-server/kit/parsers";
|
||||||
import { Users } from "@versia-server/kit/tables";
|
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, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
import { mergeAndDeduplicate } from "@/lib";
|
import { mergeAndDeduplicate } from "@/lib";
|
||||||
import { sanitizedHtmlStrip } from "@/sanitization";
|
import { sanitizedHtmlStrip } from "@/sanitization";
|
||||||
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,9 @@ import {
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { apiRoute, handleZodError, jsonOrForm } from "@versia-server/kit/api";
|
import { apiRoute, handleZodError, jsonOrForm } from "@versia-server/kit/api";
|
||||||
import { Application } from "@versia-server/kit/db";
|
import { Client } from "@versia-server/kit/db";
|
||||||
import { randomUUIDv7 } from "bun";
|
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
import { randomString } from "@/math";
|
import { randomString } from "@/math";
|
||||||
import { rateLimit } from "../../../../middlewares/rate-limit.ts";
|
import { rateLimit } from "../../../../middlewares/rate-limit.ts";
|
||||||
|
|
||||||
|
|
@ -63,15 +62,14 @@ 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 Application.insert({
|
const app = await Client.insert({
|
||||||
id: randomUUIDv7(),
|
id: randomString(32, "base64url"),
|
||||||
name: client_name,
|
name: client_name,
|
||||||
redirectUri: Array.isArray(redirect_uris)
|
redirectUris: Array.isArray(redirect_uris)
|
||||||
? redirect_uris.join("\n")
|
? redirect_uris
|
||||||
: redirect_uris,
|
: [redirect_uris],
|
||||||
scopes,
|
scopes: scopes.split(" "),
|
||||||
website: website || undefined,
|
website: website || undefined,
|
||||||
clientId: randomString(32, "base64url"),
|
|
||||||
secret: randomString(64, "base64url"),
|
secret: randomString(64, "base64url"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { apiRoute, auth } from "@versia-server/kit/api";
|
import { apiRoute, auth } from "@versia-server/kit/api";
|
||||||
import { Application } from "@versia-server/kit/db";
|
import { Client } from "@versia-server/kit/db";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { describeRoute, resolver } from "hono-openapi";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
|
|
@ -38,7 +38,7 @@ export default apiRoute((app) =>
|
||||||
async (context) => {
|
async (context) => {
|
||||||
const { token } = context.get("auth");
|
const { token } = context.get("auth");
|
||||||
|
|
||||||
const application = await Application.getFromToken(
|
const application = await Client.getFromToken(
|
||||||
token.data.accessToken,
|
token.data.accessToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Timeline } from "@versia-server/kit/db";
|
||||||
import { Users } from "@versia-server/kit/tables";
|
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, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Emoji } from "@versia-server/kit/db";
|
||||||
import { Emojis } from "@versia-server/kit/tables";
|
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, resolver } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
withEmojiParam,
|
withEmojiParam,
|
||||||
} from "@versia-server/kit/api";
|
} from "@versia-server/kit/api";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
import { mimeLookup } from "@/content_types";
|
import { mimeLookup } from "@/content_types";
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ const createImage = async (name: string): Promise<File> => {
|
||||||
.png()
|
.png()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
return new File([inputBuffer], name, {
|
return new File([inputBuffer as BlobPart], name, {
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ 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, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
import { mimeLookup } from "@/content_types";
|
import { mimeLookup } from "@/content_types";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Timeline } from "@versia-server/kit/db";
|
||||||
import { Notes } from "@versia-server/kit/tables";
|
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, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { ApiError } from "@versia-server/kit";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
||||||
import { Relationship, User } from "@versia-server/kit/db";
|
import { Relationship, User } from "@versia-server/kit/db";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { ApiError } from "@versia-server/kit";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
||||||
import { Relationship, User } from "@versia-server/kit/db";
|
import { Relationship, User } from "@versia-server/kit/db";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Timeline } from "@versia-server/kit/db";
|
||||||
import { Users } from "@versia-server/kit/tables";
|
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, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { config } from "@versia-server/config";
|
import { config } from "@versia-server/config";
|
||||||
import { apiRoute } from "@versia-server/kit/api";
|
import { apiRoute } from "@versia-server/kit/api";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { describeRoute, resolver } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { markdownToHtml } from "@versia-server/kit/markdown";
|
||||||
import { Users } from "@versia-server/kit/tables";
|
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, resolver } from "hono-openapi";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod";
|
||||||
import manifest from "../../../../../../package.json" with { type: "json" };
|
import manifest from "../../../../../../package.json" with { type: "json" };
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
|
|
@ -111,7 +111,6 @@ export default apiRoute((app) =>
|
||||||
version: "4.3.0-alpha.3+glitch",
|
version: "4.3.0-alpha.3+glitch",
|
||||||
versia_version: version,
|
versia_version: version,
|
||||||
sso: {
|
sso: {
|
||||||
forced: config.authentication.forced_openid,
|
|
||||||
providers: config.authentication.openid_providers.map(
|
providers: config.authentication.openid_providers.map(
|
||||||
(p) => ({
|
(p) => ({
|
||||||
name: p.name,
|
name: p.name,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Rule as RuleSchema } from "@versia/client/schemas";
|
||||||
import { config } from "@versia-server/config";
|
import { config } from "@versia-server/config";
|
||||||
import { apiRoute } from "@versia-server/kit/api";
|
import { apiRoute } from "@versia-server/kit/api";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { describeRoute, resolver } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { Markers } from "@versia-server/kit/tables";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { and, eq, type SQL } from "drizzle-orm";
|
import { and, eq, type SQL } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
const MarkerResponseSchema = z.object({
|
const MarkerResponseSchema = z.object({
|
||||||
notifications: MarkerSchema.optional(),
|
notifications: MarkerSchema.optional(),
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { ApiError } from "@versia-server/kit";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
||||||
import { Media } from "@versia-server/kit/db";
|
import { Media } from "@versia-server/kit/db";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { ApiError } from "@versia-server/kit";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
||||||
import { Media } from "@versia-server/kit/db";
|
import { Media } from "@versia-server/kit/db";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Timeline } from "@versia-server/kit/db";
|
||||||
import { Users } from "@versia-server/kit/tables";
|
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, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import type { Notification } from "@versia/client/schemas";
|
import type { Notification } from "@versia/client/schemas";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "@versia-server/tests";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
let notifications: z.infer<typeof Notification>[] = [];
|
let notifications: z.infer<typeof Notification>[] = [];
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { ApiError } from "@versia-server/kit";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
||||||
import { Notification } from "@versia-server/kit/db";
|
import { Notification } from "@versia-server/kit/db";
|
||||||
import { describeRoute, validator } from "hono-openapi";
|
import { describeRoute, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import type { Notification } from "@versia/client/schemas";
|
import type { Notification } from "@versia/client/schemas";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "@versia-server/tests";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
let notifications: z.infer<typeof Notification>[] = [];
|
let notifications: z.infer<typeof Notification>[] = [];
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { ApiError } from "@versia-server/kit";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
||||||
import { Notification } from "@versia-server/kit/db";
|
import { Notification } from "@versia-server/kit/db";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
getTestStatuses,
|
getTestStatuses,
|
||||||
getTestUsers,
|
getTestUsers,
|
||||||
} from "@versia-server/tests";
|
} from "@versia-server/tests";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
const statuses = await getTestStatuses(5, users[0]);
|
const statuses = await getTestStatuses(5, users[0]);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
qsQuery,
|
qsQuery,
|
||||||
} from "@versia-server/kit/api";
|
} from "@versia-server/kit/api";
|
||||||
import { describeRoute, validator } from "hono-openapi";
|
import { describeRoute, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.delete(
|
app.delete(
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { Timeline } from "@versia-server/kit/db";
|
||||||
import { Notifications } from "@versia-server/kit/tables";
|
import { Notifications } from "@versia-server/kit/tables";
|
||||||
import { and, eq, gt, gte, inArray, lt, not, sql } from "drizzle-orm";
|
import { and, eq, gt, gte, inArray, lt, not, sql } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { ApiError } from "@versia-server/kit";
|
||||||
import { apiRoute, auth } from "@versia-server/kit/api";
|
import { apiRoute, auth } from "@versia-server/kit/api";
|
||||||
import { PushSubscription } from "@versia-server/kit/db";
|
import { PushSubscription } from "@versia-server/kit/db";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { describeRoute, resolver } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.delete(
|
app.delete(
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { ApiError } from "@versia-server/kit";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
||||||
import { Role } from "@versia-server/kit/db";
|
import { Role } from "@versia-server/kit/db";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
||||||
import { Role } from "@versia-server/kit/db";
|
import { Role } from "@versia-server/kit/db";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { db } from "@versia-server/kit/db";
|
||||||
import { OpenIdAccounts } from "@versia-server/kit/tables";
|
import { OpenIdAccounts } from "@versia-server/kit/tables";
|
||||||
import { and, eq, type SQL } from "drizzle-orm";
|
import { and, eq, type SQL } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,13 @@ import { RolePermission } from "@versia/client/schemas";
|
||||||
import { config } from "@versia-server/config";
|
import { config } from "@versia-server/config";
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
||||||
import { Application, db } from "@versia-server/kit/db";
|
import { Client, db } from "@versia-server/kit/db";
|
||||||
import { OpenIdLoginFlows } from "@versia-server/kit/tables";
|
import { OpenIdLoginFlows } from "@versia-server/kit/tables";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import {
|
import * as client from "openid-client";
|
||||||
calculatePKCECodeChallenge,
|
import { z } from "zod";
|
||||||
generateRandomCodeVerifier,
|
import { oauthRedirectUri } from "@/lib";
|
||||||
} from "oauth4webapi";
|
|
||||||
import { z } from "zod/v4";
|
|
||||||
import {
|
|
||||||
oauthDiscoveryRequest,
|
|
||||||
oauthRedirectUri,
|
|
||||||
} from "../../../../plugins/openid/utils.ts";
|
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -105,25 +99,39 @@ export default apiRoute((app) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const authServer = await oauthDiscoveryRequest(new URL(issuer.url));
|
const oidcConfig = await client.discovery(
|
||||||
|
issuer.url,
|
||||||
|
issuer.client_id,
|
||||||
|
issuer.client_secret,
|
||||||
|
);
|
||||||
|
const codeVerifier = client.randomPKCECodeVerifier();
|
||||||
|
const codeChallenge =
|
||||||
|
await client.calculatePKCECodeChallenge(codeVerifier);
|
||||||
|
|
||||||
const codeVerifier = generateRandomCodeVerifier();
|
const parameters: Record<string, string> = {
|
||||||
|
scope: "openid profile email",
|
||||||
|
code_challenge: codeChallenge,
|
||||||
|
code_challenge_method: "S256",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!oidcConfig.serverMetadata().supportsPKCE()) {
|
||||||
|
parameters.state = client.randomState();
|
||||||
|
}
|
||||||
|
|
||||||
const redirectUri = oauthRedirectUri(
|
const redirectUri = oauthRedirectUri(
|
||||||
context.get("config").http.base_url,
|
context.get("config").http.base_url,
|
||||||
issuerId,
|
issuerId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const application = await Application.insert({
|
const application = await Client.insert({
|
||||||
id: randomUUIDv7(),
|
id:
|
||||||
clientId:
|
|
||||||
user.id +
|
user.id +
|
||||||
Buffer.from(
|
Buffer.from(
|
||||||
crypto.getRandomValues(new Uint8Array(32)),
|
crypto.getRandomValues(new Uint8Array(32)),
|
||||||
).toString("base64"),
|
).toString("base64"),
|
||||||
name: "Versia",
|
name: "Versia",
|
||||||
redirectUri: redirectUri.toString(),
|
redirectUris: [redirectUri.href],
|
||||||
scopes: "openid profile email",
|
scopes: ["openid", "profile", "email"],
|
||||||
secret: "",
|
secret: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -134,30 +142,28 @@ export default apiRoute((app) => {
|
||||||
.values({
|
.values({
|
||||||
id: randomUUIDv7(),
|
id: randomUUIDv7(),
|
||||||
codeVerifier,
|
codeVerifier,
|
||||||
|
state: parameters.state,
|
||||||
issuerId,
|
issuerId,
|
||||||
applicationId: application.id,
|
clientId: application.id,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
)[0];
|
)[0];
|
||||||
|
|
||||||
const codeChallenge =
|
parameters.redirect_uri = `${oauthRedirectUri(
|
||||||
await calculatePKCECodeChallenge(codeVerifier);
|
config.http.base_url,
|
||||||
|
issuerId,
|
||||||
|
)}?${new URLSearchParams({
|
||||||
|
flow: newFlow.id,
|
||||||
|
link: "true",
|
||||||
|
user_id: user.id,
|
||||||
|
})}`;
|
||||||
|
|
||||||
return context.redirect(
|
const redirectTo = client.buildAuthorizationUrl(
|
||||||
`${authServer.authorization_endpoint}?${new URLSearchParams({
|
oidcConfig,
|
||||||
client_id: issuer.client_id,
|
parameters,
|
||||||
redirect_uri: `${redirectUri}?${new URLSearchParams({
|
|
||||||
flow: newFlow.id,
|
|
||||||
link: "true",
|
|
||||||
user_id: user.id,
|
|
||||||
})}`,
|
|
||||||
response_type: "code",
|
|
||||||
scope: "openid profile email",
|
|
||||||
// PKCE
|
|
||||||
code_challenge_method: "S256",
|
|
||||||
code_challenge: codeChallenge,
|
|
||||||
}).toString()}`,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return context.redirect(redirectTo);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { Timeline } from "@versia-server/kit/db";
|
||||||
import { Users } from "@versia-server/kit/tables";
|
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, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import {
|
||||||
versiaTextToHtml,
|
versiaTextToHtml,
|
||||||
} from "@versia-server/kit/parsers";
|
} from "@versia-server/kit/parsers";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
import { sanitizedHtmlStrip } from "@/sanitization";
|
import { sanitizedHtmlStrip } from "@/sanitization";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import emojis from "unicode-emoji-json/data-ordered-emoji.json" with {
|
import emojis from "unicode-emoji-json/data-ordered-emoji.json" with {
|
||||||
type: "json",
|
type: "json",
|
||||||
};
|
};
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.put(
|
app.put(
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
import { ApiError } from "@versia-server/kit";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { apiRoute, auth, withNoteParam } from "@versia-server/kit/api";
|
import { apiRoute, auth, withNoteParam } from "@versia-server/kit/api";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { describeRoute, resolver } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
withNoteParam,
|
withNoteParam,
|
||||||
} from "@versia-server/kit/api";
|
} from "@versia-server/kit/api";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { Timeline } from "@versia-server/kit/db";
|
||||||
import { Users } from "@versia-server/kit/tables";
|
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, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Emojis } from "@versia-server/kit/tables";
|
||||||
import { generateClient, getTestUsers } from "@versia-server/tests";
|
import { generateClient, getTestUsers } from "@versia-server/tests";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(5);
|
const { users, deleteUsers } = await getTestUsers(5);
|
||||||
let media: Media;
|
let media: Media;
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import {
|
||||||
} from "@versia-server/kit/parsers";
|
} from "@versia-server/kit/parsers";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
import { sanitizedHtmlStrip } from "@/sanitization";
|
import { sanitizedHtmlStrip } from "@/sanitization";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
|
|
@ -249,7 +249,7 @@ export default apiRoute((app) =>
|
||||||
spoilerText: sanitizedSpoilerText,
|
spoilerText: sanitizedSpoilerText,
|
||||||
replyId: in_reply_to_id ?? undefined,
|
replyId: in_reply_to_id ?? undefined,
|
||||||
quotingId: quote_id ?? undefined,
|
quotingId: quote_id ?? undefined,
|
||||||
applicationId: application?.id,
|
clientId: application?.id,
|
||||||
contentSource: status,
|
contentSource: status,
|
||||||
contentType: content_type,
|
contentType: content_type,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Timeline } from "@versia-server/kit/db";
|
||||||
import { Notes } from "@versia-server/kit/tables";
|
import { Notes } from "@versia-server/kit/tables";
|
||||||
import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm";
|
import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { Timeline } from "@versia-server/kit/db";
|
||||||
import { Notes } from "@versia-server/kit/tables";
|
import { Notes } from "@versia-server/kit/tables";
|
||||||
import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm";
|
import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { db } from "@versia-server/kit/db";
|
||||||
import { FilterKeywords, Filters } from "@versia-server/kit/tables";
|
import { FilterKeywords, Filters } from "@versia-server/kit/tables";
|
||||||
import { and, eq, inArray, type SQL } from "drizzle-orm";
|
import { and, eq, inArray, type SQL } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { FilterKeywords, Filters } from "@versia-server/kit/tables";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { eq, type SQL } from "drizzle-orm";
|
import { eq, type SQL } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,6 @@ export default apiRoute((app) =>
|
||||||
hint: r.hint,
|
hint: r.hint,
|
||||||
})),
|
})),
|
||||||
sso: {
|
sso: {
|
||||||
forced: config.authentication.forced_openid,
|
|
||||||
providers: config.authentication.openid_providers.map(
|
providers: config.authentication.openid_providers.map(
|
||||||
(p) => ({
|
(p) => ({
|
||||||
name: p.name,
|
name: p.name,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { ApiError } from "@versia-server/kit";
|
||||||
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
|
||||||
import { Media } from "@versia-server/kit/db";
|
import { Media } from "@versia-server/kit/db";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { searchManager } from "@versia-server/kit/search";
|
||||||
import { Instances, Notes, Users } from "@versia-server/kit/tables";
|
import { Instances, Notes, Users } from "@versia-server/kit/tables";
|
||||||
import { and, eq, inArray, isNull, sql } from "drizzle-orm";
|
import { and, eq, inArray, isNull, sql } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { apiRoute, handleZodError } from "@versia-server/kit/api";
|
import { apiRoute, handleZodError } from "@versia-server/kit/api";
|
||||||
import { InboxJobType, inboxQueue } from "@versia-server/kit/queues/inbox";
|
import { InboxJobType, inboxQueue } from "@versia-server/kit/queues/inbox";
|
||||||
import { describeRoute, validator } from "hono-openapi";
|
import { describeRoute, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Like, User } from "@versia-server/kit/db";
|
||||||
import { Likes } from "@versia-server/kit/tables";
|
import { Likes } from "@versia-server/kit/tables";
|
||||||
import { and, eq, sql } from "drizzle-orm";
|
import { and, eq, sql } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { ApiError } from "@versia-server/kit";
|
||||||
import { apiRoute, handleZodError } from "@versia-server/kit/api";
|
import { apiRoute, handleZodError } from "@versia-server/kit/api";
|
||||||
import { file as bunFile } from "bun";
|
import { file as bunFile } from "bun";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { apiRoute, handleZodError } from "@versia-server/kit/api";
|
||||||
import { proxy } from "hono/proxy";
|
import { proxy } from "hono/proxy";
|
||||||
import type { ContentfulStatusCode, StatusCode } from "hono/utils/http-status";
|
import type { ContentfulStatusCode, StatusCode } from "hono/utils/http-status";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Note } from "@versia-server/kit/db";
|
||||||
import { Notes } from "@versia-server/kit/tables";
|
import { Notes } from "@versia-server/kit/tables";
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue