Skip to content

Commit

Permalink
Added stitcher token
Browse files Browse the repository at this point in the history
  • Loading branch information
matvp91 committed Oct 29, 2024
1 parent 0611458 commit b09ca7c
Show file tree
Hide file tree
Showing 20 changed files with 531 additions and 314 deletions.
5 changes: 2 additions & 3 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,21 @@
},
"devDependencies": {
"@types/bun": "latest",
"@types/pg": "^8.11.10",
"config": "workspace:*",
"eslint": "^9.13.0",
"typescript": "^5.6.3"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.623.0",
"@elysiajs/bearer": "^1.1.2",
"@elysiajs/cors": "^1.1.1",
"@elysiajs/eden": "^1.1.3",
"@elysiajs/jwt": "^1.1.1",
"@matvp91/elysia-swagger": "^2.0.0",
"@superstreamer/artisan": "workspace:*",
"bullmq": "^5.12.0",
"elysia": "^1.1.20",
"kysely": "^0.27.4",
"kysely-bun-sqlite": "^0.3.2",
"pg": "^8.13.1",
"shared": "workspace:*"
}
}
18 changes: 10 additions & 8 deletions packages/api/src/db/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { BunSqliteDialect } from "kysely-bun-sqlite";
import { Kysely, Migrator, FileMigrationProvider } from "kysely";
import { Database } from "bun:sqlite";
import {
Kysely,
Migrator,
FileMigrationProvider,
PostgresDialect,
} from "kysely";
import * as path from "node:path";
import * as fs from "node:fs/promises";
import { Pool } from "pg";
import { env } from "../env";
import type { KyselyDatabase } from "./types.ts";

const dialect = new BunSqliteDialect({
database: new Database(env.DATABASE),
});

export const db = new Kysely<KyselyDatabase>({
dialect,
dialect: new PostgresDialect({
pool: new Pool({ connectionString: env.DATABASE }),
}),
});

async function migrateToLatest() {
Expand Down
3 changes: 2 additions & 1 deletion packages/api/src/db/migrations/2024_10_26_init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { Kysely } from "kysely";
export async function up(db: Kysely<any>) {
await db.schema
.createTable("user")
.addColumn("id", "integer", (col) => col.primaryKey())
.addColumn("id", "serial", (col) => col.primaryKey())
.addColumn("username", "text", (col) => col.notNull())
.addColumn("password", "text", (col) => col.notNull())
.addColumn("settingAutoRefetch", "boolean", (col) => col.defaultTo(true))
.execute();

await db
Expand Down
14 changes: 13 additions & 1 deletion packages/api/src/db/repo-user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { db } from ".";
import type { UserUpdate } from "./types";

export async function getUserIdByCredentials(name: string, password: string) {
const user = await db
Expand All @@ -22,7 +23,18 @@ export async function getUserIdByCredentials(name: string, password: string) {
export async function getUser(id: number) {
return await db
.selectFrom("user")
.select(["id", "username"])
.select(["id", "username", "settingAutoRefetch"])
.where("id", "=", id)
.executeTakeFirstOrThrow();
}

export async function updateUser(
id: number,
fields: Omit<UserUpdate, "id" | "username" | "password">,
) {
return await db
.updateTable("user")
.set(fields)
.where("id", "=", id)
.executeTakeFirstOrThrow();
}
4 changes: 2 additions & 2 deletions packages/api/src/db/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Generated, Selectable, Updateable } from "kysely";
import type { Generated, Updateable } from "kysely";

export interface KyselyDatabase {
user: UserTable;
Expand All @@ -8,7 +8,7 @@ export interface UserTable {
id: Generated<number>;
username: string;
password: string;
settingAutoRefetch: boolean;
}

export type User = Selectable<UserTable>;
export type UserUpdate = Updateable<UserTable>;
50 changes: 8 additions & 42 deletions packages/api/src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,20 @@
import { Elysia, t } from "elysia";
import { jwt } from "@elysiajs/jwt";
import { env } from "../env";
import { getUserIdByCredentials } from "../db/repo-user";
import bearer from "@elysiajs/bearer";
import { bearerAuth } from "shared/auth";

export const authJwt = new Elysia().use(
jwt({
name: "authJwt",
schema: t.Union([
// User tokens describe a user interacting with the API.
t.Object({
type: t.Literal("user"),
id: t.Number(),
}),
// Service tokens, such as Stitcher.
t.Object({ type: t.Literal("service") }),
]),
secret: env.JWT_SECRET,
}),
);
const { user, jwtUser } = bearerAuth(env.JWT_SECRET);

export const authUser = new Elysia()
.use(bearer())
.use(authJwt)
.derive({ as: "scoped" }, async ({ bearer, authJwt, set }) => {
const token = await authJwt.verify(bearer);
if (!token) {
set.status = 401;
throw new Error("Unauthorized");
}
if (token.type === "user") {
return {
user: { type: "user", id: token.id },
};
}
if (token.type === "service") {
return {
user: { type: "service" },
};
}
throw new Error("Invalid token type");
});

export const auth = new Elysia().use(authJwt).post(
export const auth = new Elysia().use(jwtUser).post(
"/login",
async ({ authJwt, body, set }) => {
async ({ jwtUser, body, set }) => {
const id = await getUserIdByCredentials(body.username, body.password);
if (id === null) {
set.status = 400;
return "Unauthorized";
}
return {
token: await authJwt.sign({
token: await jwtUser.sign({
type: "user",
id,
}),
Expand All @@ -73,3 +36,6 @@ export const auth = new Elysia().use(authJwt).post(
},
},
);

// Re-export these so we can consume them in other routes.
export { user, jwtUser };
4 changes: 2 additions & 2 deletions packages/api/src/routes/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import {
} from "shared/typebox";
import { getJob, getJobs, getJobLogs } from "../jobs";
import { JobSchema } from "../types";
import { authUser } from "./auth";
import { user } from "./auth";

export const jobs = new Elysia()
.use(authUser)
.use(user)
.post(
"/transcode",
async ({ body }) => {
Expand Down
50 changes: 33 additions & 17 deletions packages/api/src/routes/profile.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
import { Elysia, t } from "elysia";
import { authUser } from "./auth";
import { getUser } from "../db/repo-user";
import { user } from "./auth";
import { getUser, updateUser } from "../db/repo-user";
import { UserSchema } from "../types";

export const profile = new Elysia().use(authUser).get(
"/profile",
async ({ user }) => {
if (user.type !== "user") {
throw new Error(`Not a user token , received "${user.type}"`);
}
return await getUser(user.id);
},
{
detail: {
summary: "Get your profile",
export const profile = new Elysia()
.use(user)
.get(
"/profile",
async ({ user }) => {
if (user.type !== "user") {
throw new Error(`Not a user token , received "${user.type}"`);
}
return await getUser(user.id);
},
response: {
200: t.Ref(UserSchema),
{
detail: {
summary: "Get your profile",
},
response: {
200: t.Ref(UserSchema),
},
},
},
);
)
.put(
"/profile",
async ({ user, body }) => {
if (user.type !== "user") {
throw new Error(`Not a user token , received "${user.type}"`);
}
await updateUser(user.id, body);
},
{
body: t.Object({
settingAutoRefetch: t.Optional(t.Boolean()),
}),
},
);
4 changes: 2 additions & 2 deletions packages/api/src/routes/storage.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Elysia, t } from "elysia";
import { authUser } from "./auth";
import { user } from "./auth";
import { getStorageFolder, getStorageFile } from "../s3";
import { StorageFolderSchema, StorageFileSchema } from "../types";

export const storage = new Elysia()
.use(authUser)
.use(user)
.get(
"/storage/folder",
async ({ query }) => {
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const UserSchema = t.Object(
{
id: t.Number(),
username: t.String(),
settingAutoRefetch: t.Boolean(),
},
{ $id: "#/components/schemas/User" },
);
Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { ReactNode } from "react";
import type { User } from "@superstreamer/api/client";

type AuthContextValue = {
token: string | null;
setToken(value: string | null): void;
user: User | null;
api: ReturnType<typeof createApiClient>;
Expand Down Expand Up @@ -62,11 +63,12 @@ export function AuthProvider({ children }: AuthProviderProps) {

const value = useMemo(() => {
return {
token,
setToken,
user,
api,
};
}, [setToken, user, api]);
}, [token, setToken, user, api]);

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useUser } from "@/AuthContext";
import {
createContext,
useState,
Expand Down Expand Up @@ -28,7 +29,8 @@ type AutoRefreshProviderProps = {
const COUNTDOWN_INTERVAL = 5;

export function AutoRefreshProvider({ children }: AutoRefreshProviderProps) {
const [active, setActive] = useState(false);
const user = useUser();
const [active, setActive] = useState(user.settingAutoRefetch);
const [countdown, setCountdown] = useState(COUNTDOWN_INTERVAL);
const [listeners] = useState(() => new Set<AutoRefreshListener>());

Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/pages/PlayerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import {
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { useAuth } from "@/AuthContext";

export function PlayerPage() {
const [schema, setSchema] = useState<object>();
const [masterUrl, setMasterUrl] = useState<string>();
const [error, setError] = useState<object>();
const { token } = useAuth();

useEffect(() => {
fetch(`${window.__ENV__.PUBLIC_STITCHER_ENDPOINT}/swagger/json`)
Expand All @@ -34,6 +36,7 @@ export function PlayerPage() {
method: "post",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body,
},
Expand Down
6 changes: 4 additions & 2 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@
"exports": {
"./env": "./src/env.ts",
"./typebox": "./src/typebox.ts",
"./lang": "./src/lang.ts"
"./lang": "./src/lang.ts",
"./auth": "./src/auth.ts"
},
"scripts": {
"lint": "eslint"
},
"dependencies": {
"@elysiajs/swagger": "^1.1.5",
"@sinclair/typebox": "^0.33.16",
"dotenv": "^16.4.5",
"elysia": "^1.1.20",
"@elysiajs/bearer": "^1.1.2",
"@elysiajs/jwt": "^1.1.1",
"find-config": "^1.0.0",
"iso-language-codes": "^2.0.0"
},
Expand Down
51 changes: 51 additions & 0 deletions packages/shared/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Elysia, t } from "elysia";
import { jwt } from "@elysiajs/jwt";
import bearer from "@elysiajs/bearer";
import type { Static } from "elysia";

const userSchema = t.Union([
// User tokens describe a user interacting with the API.
t.Object({
type: t.Literal("user"),
id: t.Number(),
}),
// Service tokens, such as Stitcher.
t.Object({ type: t.Literal("service") }),
]);

export type User = Static<typeof userSchema>;

export function bearerAuth(secret: string) {
const jwtUser = jwt({
name: "jwtUser",
schema: userSchema,
secret,
});

const user = new Elysia()
.use(bearer())
.use(jwtUser)
.derive({ as: "scoped" }, async ({ bearer, jwtUser, set }) => {
const token = await jwtUser.verify(bearer);
if (!token) {
set.status = 401;
throw new Error("Unauthorized");
}
if (token.type === "user") {
return {
user: { type: "user", id: token.id },
};
}
if (token.type === "service") {
return {
user: { type: "service" },
};
}
throw new Error("Invalid token type");
});

return {
jwtUser,
user,
};
}
Loading

0 comments on commit b09ca7c

Please sign in to comment.