mirror of
https://github.com/SrIzan10/hctv.git
synced 2026-06-06 00:56:56 +00:00
feat: scalar openapi stuff
This commit is contained in:
35
apps/web/next.openapi.json
Normal file
35
apps/web/next.openapi.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "hctv public API",
|
||||
"version": "0.0.1",
|
||||
"description": "OpenAPI specification for hackclub.tv's publicly documented API endpoints."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://localhost:3000/api",
|
||||
"description": "Local development server"
|
||||
},
|
||||
{
|
||||
"url": "https://hctv.srizan.dev/api",
|
||||
"description": "Production server"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"auth_session": {
|
||||
"type": "apiKey",
|
||||
"in": "cookie",
|
||||
"name": "auth_session"
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultResponseSet": "common",
|
||||
"apiDir": "./src/app/(ui)/(protected)/api",
|
||||
"schemaDir": "./src",
|
||||
"docsUrl": "api-docs",
|
||||
"ui": "scalar",
|
||||
"outputFile": "openapi.json",
|
||||
"includeOpenApiRoutes": false,
|
||||
"debug": false
|
||||
}
|
||||
@@ -11,7 +11,8 @@
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"ui:add": "shadcn add",
|
||||
"check-types": "tsc --noEmit"
|
||||
"check-types": "tsc --noEmit",
|
||||
"openapi": "next-openapi-gen"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hctv/auth": "*",
|
||||
@@ -33,10 +34,12 @@
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@scalar/api-reference-react": "^0.7.42",
|
||||
"@sentry/nextjs": "^10",
|
||||
"@slack/web-api": "^7.9.1",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@uploadthing/react": "^7.3.1",
|
||||
"ajv": "^8.17.1",
|
||||
"arctic": "^3.7.0",
|
||||
"bullmq": "^5.45.2",
|
||||
"cheerio": "^1.0.0",
|
||||
@@ -84,6 +87,7 @@
|
||||
"@types/ws": "^8.18.0",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.1.3",
|
||||
"next-openapi-gen": "^0.7.3",
|
||||
"postcss": "^8",
|
||||
"shadcn": "^2.7.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
|
||||
466
apps/web/public/openapi.json
Normal file
466
apps/web/public/openapi.json
Normal file
@@ -0,0 +1,466 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "hctv public API",
|
||||
"version": "0.0.1",
|
||||
"description": "OpenAPI specification for hackclub.tv's publicly documented API endpoints."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://localhost:3000/api",
|
||||
"description": "Local development server"
|
||||
},
|
||||
{
|
||||
"url": "https://hctv.srizan.dev/api",
|
||||
"description": "Production server"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"auth_session": {
|
||||
"type": "apiKey",
|
||||
"in": "cookie",
|
||||
"name": "auth_session"
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"CommitResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"commit": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"HLSPathParams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HLSResponse": {
|
||||
"oneOf": [
|
||||
{},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ArrayBuffer": {},
|
||||
"PublishForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"StreamKeyRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channel": {
|
||||
"type": "string",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"StreamKeyResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FollowParams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FollowResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"following": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FollowersParams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channel": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FollowersResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "number"
|
||||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StreamInfoResponse": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"StreamInfo": {},
|
||||
"Channel": {},
|
||||
"ThumbPathParams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ThumbResponse": {
|
||||
"oneOf": [
|
||||
{},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Buffer": {}
|
||||
},
|
||||
"responses": {}
|
||||
},
|
||||
"paths": {
|
||||
"/commit": {
|
||||
"get": {
|
||||
"operationId": "get-commit",
|
||||
"summary": "Get version.",
|
||||
"description": "Returns the current version and commit hash of the application.",
|
||||
"tags": [
|
||||
"Commit"
|
||||
],
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"commit": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/rtmp/publish": {
|
||||
"post": {
|
||||
"operationId": "post-rtmp-publish",
|
||||
"summary": "Verifies a stream key",
|
||||
"description": "Verifies a stream key and redirects to the channel name if valid.",
|
||||
"tags": [
|
||||
"Rtmp"
|
||||
],
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/rtmp/streamKey": {
|
||||
"post": {
|
||||
"operationId": "post-rtmp-streamKey",
|
||||
"summary": "Generate a new stream key",
|
||||
"description": "Generates (or regenerates) a stream key for a given channel. Requires authentication",
|
||||
"tags": [
|
||||
"Rtmp"
|
||||
],
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channel": {
|
||||
"type": "string",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/rtmp/hls/{path}": {
|
||||
"get": {
|
||||
"operationId": "get-rtmp-hls-{path}",
|
||||
"summary": "Serve HLS segments",
|
||||
"description": "Serves HLS segments from /dev/shm/hls. Requires a valid auth_session cookie.",
|
||||
"tags": [
|
||||
"Rtmp"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"example": "example"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/stream/follow": {
|
||||
"get": {
|
||||
"operationId": "get-stream-follow",
|
||||
"summary": "Follow or unfollow a channel",
|
||||
"description": "Follow or unfollow a channel. Requires authentication.",
|
||||
"tags": [
|
||||
"Stream"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "username",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"following": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"operationId": "post-stream-follow",
|
||||
"summary": "",
|
||||
"description": "",
|
||||
"tags": [
|
||||
"Stream"
|
||||
],
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/stream/info": {
|
||||
"get": {
|
||||
"operationId": "get-stream-info",
|
||||
"summary": "Get stream information",
|
||||
"description": "Retrieves stream information based on query parameters. Requires authentication for certain queries.",
|
||||
"tags": [
|
||||
"Stream"
|
||||
],
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/stream/followers/{channel}": {
|
||||
"get": {
|
||||
"operationId": "get-stream-followers-{channel}",
|
||||
"summary": "Get the number of followers for a channel",
|
||||
"description": "Retrieves the total number of followers for a specified channel.",
|
||||
"tags": [
|
||||
"Stream"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "channel",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"example": "example"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "number"
|
||||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/stream/thumb/{username}": {
|
||||
"get": {
|
||||
"operationId": "get-stream-thumb-{username}",
|
||||
"summary": "Serve user thumbnail",
|
||||
"description": "Serves user thumbnails. Requires authentication.",
|
||||
"tags": [
|
||||
"Stream"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "username",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"example": "johndoe"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,14 @@
|
||||
|
||||
type CommitResponse = {
|
||||
version: string | undefined;
|
||||
commit: string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get version.
|
||||
* @description Returns the current version and commit hash of the application.
|
||||
* @response CommitResponse
|
||||
*/
|
||||
export function GET() {
|
||||
return Response.json({
|
||||
version: process.env.version,
|
||||
|
||||
@@ -3,6 +3,18 @@ import fs from 'fs';
|
||||
import { getRedisConnection } from '@hctv/db';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
type HLSPathParams = {
|
||||
path: string;
|
||||
};
|
||||
type HLSResponse = ArrayBuffer | string;
|
||||
|
||||
/**
|
||||
* Serve HLS segments
|
||||
* @description Serves HLS segments from /dev/shm/hls. Requires a valid auth_session cookie.
|
||||
* @pathParams HLSPathParams
|
||||
* @response HLSResponse
|
||||
* @openapi
|
||||
*/
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ path: string }> }) {
|
||||
const { path } = await params;
|
||||
const c = await cookies();
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
import { prisma } from '@hctv/db';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
type PublishForm = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies a stream key
|
||||
* @description Verifies a stream key and redirects to the channel name if valid.
|
||||
* @body PublishForm
|
||||
* @contentType multipart/form-data
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const formData = await request.formData();
|
||||
const streamKey = formData.get('name')?.toString() || '';
|
||||
const streamKey = formData.get('name');
|
||||
if (typeof streamKey !== 'string') {
|
||||
return new Response('bad request', {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const key = await prisma.streamKey.findFirst({
|
||||
where: {
|
||||
|
||||
@@ -2,6 +2,20 @@ import { validateRequest } from '@/lib/auth/validate';
|
||||
import { prisma } from '@hctv/db';
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
type StreamKeyRequest = {
|
||||
channel: string;
|
||||
};
|
||||
type StreamKeyResponse = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a new stream key
|
||||
* @description Generates (or regenerates) a stream key for a given channel. Requires authentication
|
||||
* @body StreamKeyRequest
|
||||
* @response StreamKeyResponse
|
||||
* @openapi
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const { user } = await validateRequest();
|
||||
const body = await request.json();
|
||||
@@ -11,6 +25,10 @@ export async function POST(request: NextRequest) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
if (!channel || typeof channel !== 'string') {
|
||||
return new Response('Bad Request', { status: 400 });
|
||||
}
|
||||
|
||||
const channelInfo = await prisma.channel.findUnique({
|
||||
where: { name: channel },
|
||||
include: {
|
||||
|
||||
@@ -3,6 +3,19 @@ import { getNotificationQueue } from '@/lib/workers';
|
||||
import { prisma } from '@hctv/db';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
type FollowParams = {
|
||||
username: string;
|
||||
};
|
||||
type FollowResponse = {
|
||||
following: boolean;
|
||||
};
|
||||
/**
|
||||
* Follow or unfollow a channel
|
||||
* @description Follow or unfollow a channel. Requires authentication.
|
||||
* @params FollowParams
|
||||
* @response FollowResponse
|
||||
* @openapi
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const { user } = await validateRequest();
|
||||
const searchParams = new URL(request.url).searchParams;
|
||||
|
||||
@@ -2,6 +2,20 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@hctv/db';
|
||||
import { resolveChannelNameId } from '@/lib/db/resolve';
|
||||
|
||||
type FollowersParams = {
|
||||
channel: string;
|
||||
};
|
||||
type FollowersResponse = {
|
||||
count: number;
|
||||
success: boolean;
|
||||
};
|
||||
/**
|
||||
* Get the number of followers for a channel
|
||||
* @description Retrieves the total number of followers for a specified channel.
|
||||
* @pathParams FollowersParams
|
||||
* @response FollowersResponse
|
||||
* @openapi
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ channel: string }> }
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
// FIXME: THIS EFFING SUCKS OH MY GOD
|
||||
|
||||
import { validateRequest } from '@/lib/auth/validate';
|
||||
import { Prisma, prisma } from '@hctv/db';
|
||||
import { Channel, Prisma, prisma, StreamInfo } from '@hctv/db';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
type StreamInfoResponse = Array<StreamInfo & { channel: Channel }>;
|
||||
type StreamInfoQuery = {
|
||||
owned?: boolean;
|
||||
personal?: boolean;
|
||||
live?: boolean;
|
||||
};
|
||||
/**
|
||||
* Get stream information
|
||||
* @description Retrieves stream information based on query parameters. Requires authentication for certain queries.
|
||||
* @queryParams StreamInfoQuery
|
||||
* @response StreamInfoResponse
|
||||
* @openapi
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const shouldGetOwned = searchParams.get('owned') === 'true';
|
||||
|
||||
@@ -2,6 +2,18 @@ import { validateRequest } from '@/lib/auth/validate';
|
||||
import fsP from 'fs/promises';
|
||||
import fs from 'fs';
|
||||
|
||||
type ThumbPathParams = {
|
||||
username: string;
|
||||
};
|
||||
type ThumbResponse = Buffer<ArrayBufferLike> | string;
|
||||
|
||||
/**
|
||||
* Serve user thumbnail
|
||||
* @description Serves user thumbnails. Requires authentication.
|
||||
* @pathParams ThumbPathParams
|
||||
* @response ThumbResponse
|
||||
* @openapi
|
||||
*/
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ username: string }> }) {
|
||||
const { username } = await params;
|
||||
const { user } = await validateRequest();
|
||||
|
||||
16
apps/web/src/app/(ui)/(public)/api-docs/page.tsx
Normal file
16
apps/web/src/app/(ui)/(public)/api-docs/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { ApiReferenceReact } from "@scalar/api-reference-react";
|
||||
|
||||
import "@scalar/api-reference-react/style.css";
|
||||
|
||||
export default function ApiDocsPage() {
|
||||
return (
|
||||
<ApiReferenceReact
|
||||
configuration={{
|
||||
_integration: "nextjs",
|
||||
url: "/openapi.json",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user